[CI] Select mirror_hardwares presets via MIRROR_HW (H100/B200) (#5543)
Signed-off-by: wangyu <410167048@qq.com> Signed-off-by: lishunyang12 <lishunyang12@163.com> Signed-off-by: [Your Name] <your.email@example.com> Signed-off-by: wangyu <53896905+yenuo26@users.noreply.github.com> Signed-off-by: Bo Li <22713281+bobboli@users.noreply.github.com> Co-authored-by: lishunyang12 <lishunyang12@163.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Bo Li <22713281+bobboli@users.noreply.github.com>
This commit is contained in:
77 files changed
+3470
-1077
No files matched your search
@@ -3,12 +3,24 @@
|
|||||||
# ``agents`` (+ optional top-level ``image`` for NPU) + ``plugins`` (+ ``retry``
|
# ``agents`` (+ optional top-level ``image`` for NPU) + ``plugins`` (+ ``retry``
|
||||||
# for ``l4_*``), then strips ``mirror_hardwares`` before Buildkite upload.
|
# for ``l4_*``), then strips ``mirror_hardwares`` before Buildkite upload.
|
||||||
#
|
#
|
||||||
|
# Omit the key: compose {chip}_{n} from pytest -m SKU + cards_n (max of several
|
||||||
|
# positives; not cards_1 with no positive cards_* → that chip's highest existing preset).
|
||||||
|
# Unset MIRROR_HW matches H100 or L4 in -m (both → H100); no match skips the step.
|
||||||
|
# MIRROR_HW=b200 matches B200 in -m (else the step is skipped).
|
||||||
|
# MIRROR_HW must be empty or b200 (case-insensitive); unknown values fail the upload.
|
||||||
|
# CUDA preset strings (h100_4, l4_1) are omitted when MIRROR_HW=b200.
|
||||||
|
# Unset MIRROR_HW keeps string presets. NPU presets ignore MIRROR_HW.
|
||||||
|
#
|
||||||
# CUDA naming: {chip}_{count}
|
# CUDA naming: {chip}_{count}
|
||||||
# l4_1, l4_2, l4_3, l4_4, h100_1 … h100_4
|
# l4_1, l4_2, l4_3, l4_4, h100_1 … h100_4, b200_1 … b200_4
|
||||||
# NPU naming: {queue}_{resource_class}
|
# NPU naming: {queue}_{resource_class}
|
||||||
# a2b3_npu_1 — ascend-a2b3, resource_class npu-1 (A2/B3 CI image)
|
# a2b3_npu_1 — ascend-a2b3, resource_class npu-1 (A2/B3 CI image)
|
||||||
# a2b3_npu_4 — ascend-a2b3, resource_class npu-4
|
# a2b3_npu_4 — ascend-a2b3, resource_class npu-4
|
||||||
# a3_npu_2 — ascend-a3, resource_class npu-2
|
# a3_npu_2 — ascend-a3, resource_class npu-2 (A3 CI image tag …:$BUILDKITE_COMMIT-a3)
|
||||||
|
#
|
||||||
|
# B200 (b200-k8s): matches vLLM device ``b200-k8s`` from ci-infra
|
||||||
|
# (AgentQueue.B200_K8S + b200_plugin_template). Queue routes the K8s pool;
|
||||||
|
# podSpec uses hostNetwork + /raid|/mnt/shared (no H100-style nodeSelector).
|
||||||
#
|
#
|
||||||
# The ``l4_*`` presets run on the EKS ``l4-k8s`` queue (agent-stack-k8s),
|
# The ``l4_*`` presets run on the EKS ``l4-k8s`` queue (agent-stack-k8s),
|
||||||
# packed onto g6.12xlarge L4 nodes (``vllm.ci/gpu-pool=l4x4``). GPU / CPU /
|
# packed onto g6.12xlarge L4 nodes (``vllm.ci/gpu-pool=l4x4``). GPU / CPU /
|
||||||
@@ -380,6 +392,211 @@ mirror_hardwares:
|
|||||||
path: /mnt/hf-cache
|
path: /mnt/hf-cache
|
||||||
type: DirectoryOrCreate
|
type: DirectoryOrCreate
|
||||||
|
|
||||||
|
# vLLM device: b200-k8s → agents.queue b200-k8s + Buildkite K8s agent-stack podSpec
|
||||||
|
b200_1:
|
||||||
|
agents:
|
||||||
|
queue: b200-k8s
|
||||||
|
plugins:
|
||||||
|
- kubernetes:
|
||||||
|
podSpec:
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: k8s-ecr-login-renew-docker-secret
|
||||||
|
containers:
|
||||||
|
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
nvidia.com/gpu: 1
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
add:
|
||||||
|
- IPC_LOCK
|
||||||
|
- SYS_RESOURCE
|
||||||
|
volumeMounts:
|
||||||
|
- name: devshm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
- name: raid
|
||||||
|
mountPath: /raid
|
||||||
|
- name: shared
|
||||||
|
mountPath: /mnt/shared
|
||||||
|
env:
|
||||||
|
- name: NCCL_CUMEM_HOST_ENABLE
|
||||||
|
value: "0"
|
||||||
|
- name: HF_HOME
|
||||||
|
value: /raid/hf_cache
|
||||||
|
- name: HF_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hf-token-secret
|
||||||
|
key: token
|
||||||
|
volumes:
|
||||||
|
- name: devshm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
- name: raid
|
||||||
|
hostPath:
|
||||||
|
path: /raid
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: shared
|
||||||
|
hostPath:
|
||||||
|
path: /mnt/shared
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
|
||||||
|
b200_2:
|
||||||
|
agents:
|
||||||
|
queue: b200-k8s
|
||||||
|
plugins:
|
||||||
|
- kubernetes:
|
||||||
|
podSpec:
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: k8s-ecr-login-renew-docker-secret
|
||||||
|
containers:
|
||||||
|
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
nvidia.com/gpu: 2
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
add:
|
||||||
|
- IPC_LOCK
|
||||||
|
- SYS_RESOURCE
|
||||||
|
volumeMounts:
|
||||||
|
- name: devshm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
- name: raid
|
||||||
|
mountPath: /raid
|
||||||
|
- name: shared
|
||||||
|
mountPath: /mnt/shared
|
||||||
|
env:
|
||||||
|
- name: NCCL_CUMEM_HOST_ENABLE
|
||||||
|
value: "0"
|
||||||
|
- name: HF_HOME
|
||||||
|
value: /raid/hf_cache
|
||||||
|
- name: HF_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hf-token-secret
|
||||||
|
key: token
|
||||||
|
volumes:
|
||||||
|
- name: devshm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
- name: raid
|
||||||
|
hostPath:
|
||||||
|
path: /raid
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: shared
|
||||||
|
hostPath:
|
||||||
|
path: /mnt/shared
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
|
||||||
|
b200_3:
|
||||||
|
agents:
|
||||||
|
queue: b200-k8s
|
||||||
|
plugins:
|
||||||
|
- kubernetes:
|
||||||
|
podSpec:
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: k8s-ecr-login-renew-docker-secret
|
||||||
|
containers:
|
||||||
|
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
nvidia.com/gpu: 3
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
add:
|
||||||
|
- IPC_LOCK
|
||||||
|
- SYS_RESOURCE
|
||||||
|
volumeMounts:
|
||||||
|
- name: devshm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
- name: raid
|
||||||
|
mountPath: /raid
|
||||||
|
- name: shared
|
||||||
|
mountPath: /mnt/shared
|
||||||
|
env:
|
||||||
|
- name: NCCL_CUMEM_HOST_ENABLE
|
||||||
|
value: "0"
|
||||||
|
- name: HF_HOME
|
||||||
|
value: /raid/hf_cache
|
||||||
|
- name: HF_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hf-token-secret
|
||||||
|
key: token
|
||||||
|
volumes:
|
||||||
|
- name: devshm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
- name: raid
|
||||||
|
hostPath:
|
||||||
|
path: /raid
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: shared
|
||||||
|
hostPath:
|
||||||
|
path: /mnt/shared
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
|
||||||
|
b200_4:
|
||||||
|
agents:
|
||||||
|
queue: b200-k8s
|
||||||
|
plugins:
|
||||||
|
- kubernetes:
|
||||||
|
podSpec:
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: k8s-ecr-login-renew-docker-secret
|
||||||
|
containers:
|
||||||
|
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
nvidia.com/gpu: 4
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
add:
|
||||||
|
- IPC_LOCK
|
||||||
|
- SYS_RESOURCE
|
||||||
|
volumeMounts:
|
||||||
|
- name: devshm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
- name: raid
|
||||||
|
mountPath: /raid
|
||||||
|
- name: shared
|
||||||
|
mountPath: /mnt/shared
|
||||||
|
env:
|
||||||
|
- name: NCCL_CUMEM_HOST_ENABLE
|
||||||
|
value: "0"
|
||||||
|
- name: HF_HOME
|
||||||
|
value: /raid/hf_cache
|
||||||
|
- name: HF_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: hf-token-secret
|
||||||
|
key: token
|
||||||
|
volumes:
|
||||||
|
- name: devshm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
- name: raid
|
||||||
|
hostPath:
|
||||||
|
path: /raid
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: shared
|
||||||
|
hostPath:
|
||||||
|
path: /mnt/shared
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
|
||||||
a2b3_npu_1:
|
a2b3_npu_1:
|
||||||
agents:
|
agents:
|
||||||
queue: "ascend-a2b3"
|
queue: "ascend-a2b3"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""Shared Buildkite skip-ci and CI-YAML level targeting logic.
|
"""Shared Buildkite skip-ci and CI-YAML level targeting logic.
|
||||||
|
|
||||||
``resolve_ci_decision(changed_files, ...)`` buckets explicit file lists (tests, dry-run).
|
``resolve_ci_decision(changed_files, ...)`` buckets explicit file lists (tests, dry-run).
|
||||||
@@ -10,12 +11,6 @@ CLI (for bootstrap scripts; exit code is the signal):
|
|||||||
python3 skip_ci.py gate <platform> <level>
|
python3 skip_ci.py gate <platform> <level>
|
||||||
Exit 0 when bootstrap should stop (skip-all, or that L2/L3 target is off).
|
Exit 0 when bootstrap should stop (skip-all, or that L2/L3 target is off).
|
||||||
Prints ``skip-all`` or ``skip-l23`` on stdout; logs the decision once.
|
Prints ``skip-all`` or ``skip-l23`` on stdout; logs the decision once.
|
||||||
python3 skip_ci.py check-skip-all
|
|
||||||
Exit 0 when only docs/skip-mark changes → bootstrap skips entire CI upload.
|
|
||||||
python3 skip_ci.py check-skip-l2-l3 <platform> <level>
|
|
||||||
Exit 0 when that L2/L3 pipeline should be skipped (CI config YAML-only diffs).
|
|
||||||
python3 skip_ci.py print-annotate
|
|
||||||
Print decision message for Buildkite annotation.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -258,11 +253,10 @@ class CiDecision:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CiContext:
|
class CiContext:
|
||||||
"""Skip-ci decision plus git diff inputs used to compute it."""
|
"""Skip-ci decision plus changed files used for pipeline filtering."""
|
||||||
|
|
||||||
decision: CiDecision
|
decision: CiDecision
|
||||||
changed_files: list[str] | None
|
changed_files: list[str] | None
|
||||||
diff_range: str | None
|
|
||||||
|
|
||||||
|
|
||||||
def _log(message: str) -> None:
|
def _log(message: str) -> None:
|
||||||
@@ -332,7 +326,7 @@ def resolve_ci_context_from_git() -> CiContext:
|
|||||||
diff_range = _resolve_diff_range()
|
diff_range = _resolve_diff_range()
|
||||||
changed_files = _changed_files_for_diff_range(diff_range)
|
changed_files = _changed_files_for_diff_range(diff_range)
|
||||||
decision = resolve_ci_decision(changed_files, diff_range=diff_range)
|
decision = resolve_ci_decision(changed_files, diff_range=diff_range)
|
||||||
return CiContext(decision=decision, changed_files=changed_files, diff_range=diff_range)
|
return CiContext(decision=decision, changed_files=changed_files)
|
||||||
|
|
||||||
|
|
||||||
# --- Doc / skip-mark helpers ---
|
# --- Doc / skip-mark helpers ---
|
||||||
@@ -571,23 +565,6 @@ def main() -> int:
|
|||||||
gate_parser.add_argument("platform", help="cuda, amd, intel, or npu")
|
gate_parser.add_argument("platform", help="cuda, amd, intel, or npu")
|
||||||
gate_parser.add_argument("level", choices=("l2", "l3"), help="test level")
|
gate_parser.add_argument("level", choices=("l2", "l3"), help="test level")
|
||||||
|
|
||||||
subparsers.add_parser(
|
|
||||||
"check-skip-all",
|
|
||||||
help="exit 0 when docs/skip-mark-only diff should skip entire CI upload",
|
|
||||||
)
|
|
||||||
|
|
||||||
skip_parser = subparsers.add_parser(
|
|
||||||
"check-skip-l2-l3",
|
|
||||||
help="exit 0 when PLATFORM l2/l3 pipeline should be skipped (CI config YAML-only diffs)",
|
|
||||||
)
|
|
||||||
skip_parser.add_argument("platform", help="cuda, amd, intel, or npu")
|
|
||||||
skip_parser.add_argument("level", choices=("l2", "l3"), help="test level")
|
|
||||||
|
|
||||||
subparsers.add_parser(
|
|
||||||
"print-annotate",
|
|
||||||
help="print human-readable skip reason for Buildkite annotation",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
decision = resolve_ci_context_from_git().decision
|
decision = resolve_ci_context_from_git().decision
|
||||||
|
|
||||||
@@ -600,16 +577,6 @@ def main() -> int:
|
|||||||
return 0
|
return 0
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if args.command == "check-skip-all":
|
|
||||||
return 0 if decision.skip_all else 1
|
|
||||||
|
|
||||||
if args.command == "check-skip-l2-l3":
|
|
||||||
return 0 if not decision.is_run(args.platform, args.level) else 1
|
|
||||||
|
|
||||||
if args.command == "print-annotate":
|
|
||||||
print(decision.message or "CI will run normally")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,19 @@ Bootstrap mode (``bootstrap-upload-steps.yml``):
|
|||||||
|
|
||||||
Test pipeline mode (e.g. test-merge.yml):
|
Test pipeline mode (e.g. test-merge.yml):
|
||||||
- Drop steps whose ``source_file_dependencies`` do not match changed files.
|
- Drop steps whose ``source_file_dependencies`` do not match changed files.
|
||||||
- Expand uploader-only ``mirror_hardwares: l4_1`` into ``agents`` (+ optional ``image``
|
- Expand uploader-only ``mirror_hardwares`` into ``agents`` (+ optional ``image``
|
||||||
for NPU) + ``plugins`` (see ci_mirror_hardwares.yml).
|
for NPU) + ``plugins`` (see ci_mirror_hardwares.yml).
|
||||||
|
- Omit ``mirror_hardwares`` to compose ``{chip}_{n}`` from pytest ``-m`` SKU
|
||||||
|
markers plus ``cards_n`` (max of several positives; ``not cards_1`` with no
|
||||||
|
positive ``cards_*`` uses that chip's highest existing preset). An explicit
|
||||||
|
preset string (``h100_2``) is used as-is (marks are ignored). Names that
|
||||||
|
are not keys in ``ci_mirror_hardwares.yml`` fail the upload.
|
||||||
|
|
||||||
|
Inferred chip: unset ``MIRROR_HW`` matches ``H100`` or ``L4`` in ``-m``
|
||||||
|
(both → H100); no match skips the step. ``MIRROR_HW=b200`` matches
|
||||||
|
``B200`` in ``-m`` (otherwise skipped). ``-m`` is not rewritten.
|
||||||
|
``MIRROR_HW`` must be empty or ``b200``; unknown values fail the upload.
|
||||||
|
A CUDA preset string such as ``h100_4`` is omitted when ``MIRROR_HW=b200``.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 upload_pipeline.py [--upload] [--all | --e2e] <pipeline.yml>
|
python3 upload_pipeline.py [--upload] [--all | --e2e] <pipeline.yml>
|
||||||
@@ -25,11 +36,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import copy
|
import copy
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
@@ -45,6 +58,14 @@ from skip_ci import (
|
|||||||
resolve_ci_context_from_git,
|
resolve_ci_context_from_git,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from tests.helpers.mark import ( # noqa: E402
|
||||||
|
get_skus_for_platform,
|
||||||
|
get_supported_card_counts,
|
||||||
|
)
|
||||||
|
|
||||||
# --- Constants ---
|
# --- Constants ---
|
||||||
|
|
||||||
LOG = "upload_pipeline"
|
LOG = "upload_pipeline"
|
||||||
@@ -233,28 +254,249 @@ def _load_mirror_hardwares() -> dict[str, dict[str, Any]]:
|
|||||||
return presets
|
return presets
|
||||||
|
|
||||||
|
|
||||||
def _expand_mirror_hardwares(step: dict[str, Any]) -> dict[str, Any]:
|
@lru_cache(maxsize=1)
|
||||||
"""Replace uploader-only ``mirror_hardwares`` with preset fields from ci_mirror_hardwares.yml."""
|
def _cuda_mirror_chips() -> tuple[str, ...]:
|
||||||
hardware = step.get("mirror_hardwares")
|
"""Lowercase CUDA SKUs that have at least one ``{chip}_{n}`` preset.
|
||||||
if hardware is None:
|
|
||||||
return step
|
|
||||||
|
|
||||||
if not isinstance(hardware, str) or not hardware.strip():
|
Longer tokens first so ``h100`` is not matched as a prefix of ``h200``.
|
||||||
|
"""
|
||||||
|
skus = get_skus_for_platform("cuda")
|
||||||
|
known = _load_mirror_hardwares()
|
||||||
|
chips = {
|
||||||
|
sku.lower() for sku in skus if any(name == sku.lower() or name.startswith(f"{sku.lower()}_") for name in known)
|
||||||
|
}
|
||||||
|
return tuple(sorted(chips, key=lambda chip: (-len(chip), chip)))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_cards_marks(expr: str) -> tuple[set[int], bool]:
|
||||||
|
"""Parse registered ``cards_n`` / ``not cards_n`` from a pytest ``-m`` expr.
|
||||||
|
|
||||||
|
Returns positive card counts and whether any ``not cards_*`` is present.
|
||||||
|
"""
|
||||||
|
counts = get_supported_card_counts()
|
||||||
|
alt = "|".join(str(n) for n in sorted(counts, key=lambda n: (-len(str(n)), -n))) if counts else "(?!)"
|
||||||
|
positives: set[int] = set()
|
||||||
|
has_not_cards = False
|
||||||
|
for match in re.finditer(rf"\b(?:(not)\s+)?cards_({alt})\b", expr):
|
||||||
|
if match.group(1):
|
||||||
|
has_not_cards = True
|
||||||
|
else:
|
||||||
|
positives.add(int(match.group(2)))
|
||||||
|
return positives, has_not_cards
|
||||||
|
|
||||||
|
|
||||||
|
def _read_hardware_marks(expr: str) -> set[str]:
|
||||||
|
"""Return lowercase positive CUDA SKUs that have a mirror preset.
|
||||||
|
|
||||||
|
``not H100`` is ignored. SKUs with no ``{chip}_*`` preset (e.g. H200) are dropped.
|
||||||
|
"""
|
||||||
|
skus = sorted(get_skus_for_platform("cuda"), key=lambda s: (-len(s), s))
|
||||||
|
if not skus:
|
||||||
|
return set()
|
||||||
|
alt = "|".join(re.escape(sku) for sku in skus)
|
||||||
|
mirror = set(_cuda_mirror_chips())
|
||||||
|
chips: set[str] = set()
|
||||||
|
for match in re.finditer(rf"\b(?:(not)\s+)?({alt})\b", expr):
|
||||||
|
if match.group(1):
|
||||||
|
continue
|
||||||
|
chip = match.group(2).lower()
|
||||||
|
if chip in mirror:
|
||||||
|
chips.add(chip)
|
||||||
|
return chips
|
||||||
|
|
||||||
|
|
||||||
|
# CI policy: not pytest-mark registry.
|
||||||
|
_SUPPORTED_MIRROR_HW_SELECTORS = frozenset({"b200"})
|
||||||
|
# Unset MIRROR_HW: match H100 then L4 in -m (skip if neither).
|
||||||
|
_DEFAULT_INFER_CHIPS = ("h100", "l4")
|
||||||
|
_PYTEST_MARKER_ARG = re.compile(r"-m\s+(?:\"([^\"]*)\"|'([^']*)'|(\S+))")
|
||||||
|
_CARDS_CHIP_MAX: Literal["max"] = "max"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mirror_hw_selector() -> str:
|
||||||
|
"""Return lowercase ``MIRROR_HW``, or empty to keep string presets / marker chips.
|
||||||
|
|
||||||
|
A set value must be ``b200`` (case-insensitive). Unknown values fail closed
|
||||||
|
so a typo cannot silently drop the CUDA pipeline.
|
||||||
|
"""
|
||||||
|
selector = os.environ.get("MIRROR_HW", "").strip().lower()
|
||||||
|
if not selector:
|
||||||
|
return ""
|
||||||
|
if selector not in _SUPPORTED_MIRROR_HW_SELECTORS:
|
||||||
|
allowed = " or ".join(repr(s) for s in sorted(_SUPPORTED_MIRROR_HW_SELECTORS))
|
||||||
|
raise ValueError(f"unsupported MIRROR_HW={selector!r}; expected empty or {allowed}")
|
||||||
|
return selector
|
||||||
|
|
||||||
|
|
||||||
|
def _read_pytest_marks(commands: Any) -> tuple[set[str], int | Literal["max"] | None]:
|
||||||
|
"""Read pytest ``-m`` from step commands: CUDA SKU chips and cards count.
|
||||||
|
|
||||||
|
*chips* are lowercase SKUs from positive markers (``not H100`` is ignored).
|
||||||
|
*cards* is the max positive ``cards_n``, ``"max"`` when only ``not cards_*``
|
||||||
|
is present, or ``None``.
|
||||||
|
"""
|
||||||
|
chunks: list[str] = []
|
||||||
|
|
||||||
|
def _collect(value: Any) -> None:
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
if isinstance(value, str):
|
||||||
|
chunks.append(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for part in value:
|
||||||
|
_collect(part)
|
||||||
|
else:
|
||||||
|
chunks.append(str(value))
|
||||||
|
|
||||||
|
_collect(commands)
|
||||||
|
text = "\n".join(chunks)
|
||||||
|
|
||||||
|
chips: set[str] = set()
|
||||||
|
positives: set[int] = set()
|
||||||
|
has_not_cards = False
|
||||||
|
for match in _PYTEST_MARKER_ARG.finditer(text):
|
||||||
|
expr = match.group(1) or match.group(2) or match.group(3) or ""
|
||||||
|
chips |= _read_hardware_marks(expr)
|
||||||
|
card_counts, not_cards = _read_cards_marks(expr)
|
||||||
|
positives.update(card_counts)
|
||||||
|
has_not_cards = has_not_cards or not_cards
|
||||||
|
|
||||||
|
if positives:
|
||||||
|
cards: int | Literal["max"] | None = max(positives)
|
||||||
|
elif has_not_cards:
|
||||||
|
cards = _CARDS_CHIP_MAX
|
||||||
|
else:
|
||||||
|
cards = None
|
||||||
|
return chips, cards
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_mirror_hardware_name(
|
||||||
|
chips: set[str],
|
||||||
|
cards: int | Literal["max"] | None,
|
||||||
|
*,
|
||||||
|
step_label: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""Assemble ``{chip}_{n}`` from pytest ``-m`` chips/cards, or None to skip.
|
||||||
|
|
||||||
|
Unset ``MIRROR_HW``: ``H100`` if in ``-m``, else ``L4``, else skip.
|
||||||
|
Set ``MIRROR_HW``: that chip if in ``-m``, else skip.
|
||||||
|
``cards`` is a registered ``cards_n``, ``"max"`` for that chip's highest
|
||||||
|
existing preset, or None (error after a chip matched — no ``cards_*``).
|
||||||
|
"""
|
||||||
|
selector = _get_mirror_hw_selector()
|
||||||
|
if selector:
|
||||||
|
chip = selector if selector in chips else None
|
||||||
|
skip_reason = f"MIRROR_HW={selector!r} not in -m SKUs {sorted(chips)}"
|
||||||
|
else:
|
||||||
|
chip = next((c for c in _DEFAULT_INFER_CHIPS if c in chips), None)
|
||||||
|
skip_reason = f"no H100 or L4 in -m SKUs {sorted(chips)}"
|
||||||
|
if chip is None:
|
||||||
|
_log(f"skip {step_label}: {skip_reason}")
|
||||||
|
return None
|
||||||
|
if cards is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"mirror_hardwares must be a non-empty string in step {_get_step_label(step)!r}",
|
f"step {step_label!r} has a pytest -m SKU marker but no cards_* "
|
||||||
|
f"(or not cards_*); add cards_n or set mirror_hardwares to force a preset",
|
||||||
)
|
)
|
||||||
|
|
||||||
preset = _load_mirror_hardwares().get(hardware)
|
count_source = "not cards_*" if cards == _CARDS_CHIP_MAX else f"cards_{cards}"
|
||||||
|
count = None if cards == _CARDS_CHIP_MAX else cards
|
||||||
|
registered = get_supported_card_counts()
|
||||||
|
if count is not None and count not in registered:
|
||||||
|
supported = ", ".join(f"cards_{n}" for n in sorted(registered)) or "(none)"
|
||||||
|
raise ValueError(
|
||||||
|
f"{count_source} in step {step_label!r} is not a registered cards_* mark; supported: {supported}",
|
||||||
|
)
|
||||||
|
known = _load_mirror_hardwares()
|
||||||
|
if count is None:
|
||||||
|
available = [n for n in sorted(registered) if f"{chip}_{n}" in known]
|
||||||
|
if not available:
|
||||||
|
raise ValueError(
|
||||||
|
f"{count_source} in step {step_label!r} has no CUDA count presets for {chip}",
|
||||||
|
)
|
||||||
|
count = max(available)
|
||||||
|
chosen = f"{chip}_{count}"
|
||||||
|
if chosen not in known:
|
||||||
|
raise ValueError(
|
||||||
|
f"{count_source} in step {step_label!r} has no preset {chosen!r}",
|
||||||
|
)
|
||||||
|
_log(f"{step_label}: {count_source} → {chosen!r}")
|
||||||
|
return chosen
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_mirror_hardware_name(hardware: Any, *, step_label: str) -> str | None:
|
||||||
|
"""Map ``mirror_hardwares`` to a ``ci_mirror_hardwares.yml`` preset, or None to skip.
|
||||||
|
|
||||||
|
The value is a preset name (``h100_2``). Unknown names fail later when the
|
||||||
|
registry is loaded. CUDA names are omitted when ``MIRROR_HW`` names a
|
||||||
|
different chip; NPU strings ignore ``MIRROR_HW``.
|
||||||
|
"""
|
||||||
|
if isinstance(hardware, bool):
|
||||||
|
raise ValueError(f"mirror_hardwares must not be a boolean in step {step_label!r}")
|
||||||
|
if isinstance(hardware, str):
|
||||||
|
name = hardware.strip()
|
||||||
|
elif isinstance(hardware, int):
|
||||||
|
name = str(hardware)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"mirror_hardwares must be a preset string in step {step_label!r}",
|
||||||
|
)
|
||||||
|
if not name:
|
||||||
|
raise ValueError(f"mirror_hardwares must be a non-empty string in step {step_label!r}")
|
||||||
|
|
||||||
|
selector = _get_mirror_hw_selector()
|
||||||
|
token = name.lower()
|
||||||
|
chip = next(
|
||||||
|
(c for c in _cuda_mirror_chips() if token == c or token.startswith(f"{c}_")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if selector and chip is not None and chip != selector:
|
||||||
|
_log(
|
||||||
|
f"skip {step_label}: preset {name!r} is {chip} hardware; MIRROR_HW={selector!r}",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_mirror_hardwares(step: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
"""Replace ``mirror_hardwares`` (or infer it from ``-m``) with preset agents/plugins.
|
||||||
|
|
||||||
|
Explicit preset string: used as-is; pytest marks are ignored.
|
||||||
|
Omitted key: match H100/L4 (or ``MIRROR_HW``) in ``-m``, then compose
|
||||||
|
``{chip}_{n}`` from ``cards_n``. No match skips the step. Steps with no
|
||||||
|
SKU/cards and no ``mirror_hardwares`` are left unchanged (CPU jobs).
|
||||||
|
|
||||||
|
Returns None when the current ``MIRROR_HW`` selector does not apply.
|
||||||
|
"""
|
||||||
|
step_label = _get_step_label(step)
|
||||||
|
has_pool = step.get("agents") is not None or step.get("plugins") is not None or step.get("image") is not None
|
||||||
|
|
||||||
|
if "mirror_hardwares" not in step:
|
||||||
|
if has_pool:
|
||||||
|
return step
|
||||||
|
chips, cards = _read_pytest_marks(step.get("commands"))
|
||||||
|
if not chips and cards is None:
|
||||||
|
return step
|
||||||
|
preset_name = _compose_mirror_hardware_name(chips, cards, step_label=step_label)
|
||||||
|
else:
|
||||||
|
if has_pool:
|
||||||
|
raise ValueError(
|
||||||
|
f"step {step_label!r} sets mirror_hardwares together with agents/plugins/image; "
|
||||||
|
f"use mirror_hardwares only",
|
||||||
|
)
|
||||||
|
preset_name = _resolve_mirror_hardware_name(
|
||||||
|
step.get("mirror_hardwares"),
|
||||||
|
step_label=step_label,
|
||||||
|
)
|
||||||
|
|
||||||
|
if preset_name is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
preset = _load_mirror_hardwares().get(preset_name)
|
||||||
if preset is None:
|
if preset is None:
|
||||||
known = ", ".join(sorted(_load_mirror_hardwares()))
|
known = ", ".join(sorted(_load_mirror_hardwares()))
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"unknown mirror_hardwares {hardware!r} in step {_get_step_label(step)!r}; known: {known}",
|
f"unknown mirror_hardwares {preset_name!r} in step {step_label!r}; known: {known}",
|
||||||
)
|
|
||||||
|
|
||||||
if step.get("agents") is not None or step.get("plugins") is not None or step.get("image") is not None:
|
|
||||||
raise ValueError(
|
|
||||||
f"step {_get_step_label(step)!r} sets mirror_hardwares together with agents/plugins/image; "
|
|
||||||
"use mirror_hardwares only",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expanded = copy.deepcopy(preset)
|
expanded = copy.deepcopy(preset)
|
||||||
@@ -301,7 +543,7 @@ def _process_test_steps(
|
|||||||
nested = step.get("steps")
|
nested = step.get("steps")
|
||||||
if nested is not None:
|
if nested is not None:
|
||||||
kept_nested = _process_test_steps(nested, changed_files)
|
kept_nested = _process_test_steps(nested, changed_files)
|
||||||
if changed_files is not None and not kept_nested:
|
if not kept_nested:
|
||||||
_log(f"omit empty group {_get_step_label(step)!r}")
|
_log(f"omit empty group {_get_step_label(step)!r}")
|
||||||
continue
|
continue
|
||||||
new_step = {key: value for key, value in step.items() if key != "source_file_dependencies"}
|
new_step = {key: value for key, value in step.items() if key != "source_file_dependencies"}
|
||||||
@@ -309,14 +551,11 @@ def _process_test_steps(
|
|||||||
processed.append(new_step)
|
processed.append(new_step)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if deps is not None:
|
leaf = {key: value for key, value in step.items() if key != "source_file_dependencies"}
|
||||||
processed.append(
|
expanded = _expand_mirror_hardwares(leaf)
|
||||||
_expand_mirror_hardwares(
|
if expanded is None:
|
||||||
{key: value for key, value in step.items() if key != "source_file_dependencies"},
|
continue
|
||||||
),
|
processed.append(expanded)
|
||||||
)
|
|
||||||
else:
|
|
||||||
processed.append(_expand_mirror_hardwares(step))
|
|
||||||
|
|
||||||
return processed
|
return processed
|
||||||
|
|
||||||
@@ -342,6 +581,10 @@ def _render_test_pipeline(
|
|||||||
e2e_only: bool = False,
|
e2e_only: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Filter steps by PR diff and strip uploader-only ``source_file_dependencies`` metadata."""
|
"""Filter steps by PR diff and strip uploader-only ``source_file_dependencies`` metadata."""
|
||||||
|
# Validate MIRROR_HW once up front. Per-step skip must not run for typos
|
||||||
|
# (e.g. b20o), or a pipeline can shrink to leftover CPU-only steps and
|
||||||
|
# still upload successfully.
|
||||||
|
_get_mirror_hw_selector()
|
||||||
steps = doc.get("steps")
|
steps = doc.get("steps")
|
||||||
if not isinstance(steps, list):
|
if not isinstance(steps, list):
|
||||||
return doc
|
return doc
|
||||||
@@ -371,10 +614,7 @@ def _render_pipeline(
|
|||||||
|
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
ctx = resolve_ci_context_from_git()
|
ctx = resolve_ci_context_from_git()
|
||||||
if force_all or e2e_only:
|
changed_files = None if force_all or e2e_only else ctx.changed_files
|
||||||
changed_files = None
|
|
||||||
else:
|
|
||||||
changed_files = ctx.changed_files
|
|
||||||
|
|
||||||
doc = yaml.safe_load(text)
|
doc = yaml.safe_load(text)
|
||||||
if not isinstance(doc, dict):
|
if not isinstance(doc, dict):
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# CUDA CI L3 (`advanced_model` e2e) plus L1 CPU Simple Test (`core_model and cpu`).
|
||||||
|
# Uploaded by Upload Merge Pipeline: PR label `merge-test`, or post-merge on main.
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# CUDA CI L4 (`full_model`): nightly and release share this file.
|
||||||
|
# Uploaded by Upload Nightly Pipeline: NIGHTLY=1, or PR labels (nightly-test / omni-test / ...).
|
||||||
|
# Hardware: omit mirror_hardwares to compose from -m SKU + cards_n; or set a preset string to force.
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||||
@@ -15,14 +19,12 @@ steps:
|
|||||||
- label: ":full_moon: Omni · Function Test with H100 · Single-GPU"
|
- label: ":full_moon: Omni · Function Test with H100 · Single-GPU"
|
||||||
timeout_in_minutes: 120
|
timeout_in_minutes: 120
|
||||||
commands:
|
commands:
|
||||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and omni and cards_1" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
|
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and B200 and omni and cards_1" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
|
||||||
mirror_hardwares: h100_1
|
|
||||||
|
|
||||||
- label: ":full_moon: Omni · Function Test with H100 · 2-GPU"
|
- label: ":full_moon: Omni · Function Test with H100 · 2-GPU"
|
||||||
timeout_in_minutes: 90
|
timeout_in_minutes: 90
|
||||||
commands:
|
commands:
|
||||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and omni and cards_2" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py
|
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and B200 and omni and cards_2" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py
|
||||||
mirror_hardwares: h100_2
|
|
||||||
|
|
||||||
- label: ":full_moon: Omni · MiniCPM-o 4.5 Duplex Test"
|
- label: ":full_moon: Omni · MiniCPM-o 4.5 Duplex Test"
|
||||||
timeout_in_minutes: 50
|
timeout_in_minutes: 50
|
||||||
@@ -266,7 +268,6 @@ steps:
|
|||||||
timeout_in_minutes: 180
|
timeout_in_minutes: 180
|
||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
|
||||||
- export CACHE_DIT_VERSION=1.5.0
|
- export CACHE_DIT_VERSION=1.5.0
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
@@ -369,7 +370,6 @@ steps:
|
|||||||
timeout_in_minutes: 180
|
timeout_in_minutes: 180
|
||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json -m "H100 and cards_1"
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json -m "H100 and cards_1"
|
||||||
@@ -419,7 +419,6 @@ steps:
|
|||||||
timeout_in_minutes: 180
|
timeout_in_minutes: 180
|
||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_minimax_h3_vllm_omni.json
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_minimax_h3_vllm_omni.json
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# CUDA CI L2 (`core_model` e2e) plus L1 CPU Simple Test (`core_model and cpu`).
|
||||||
|
# Uploaded by Upload Ready Pipeline: PR label `ready`, or main+WEEKLY=1 with --e2e.
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# CUDA CI L5 (`slow` / reliability / extra coverage).
|
||||||
|
# Uploaded by Upload Weekly Pipeline: WEEKLY=1 / NON_CRITICAL=1, or PR label `weekly-test`.
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||||
|
|||||||
@@ -57,13 +57,11 @@ Canonical layout (prefer these paths for new changes):
|
|||||||
| Bootstrap vs test YAML | **Bootstrap** (`pipeline*.yml`) builds images and uploads **child** test pipelines. **Test** YAML (`test-*.yml`) lists pytest steps only. |
|
| Bootstrap vs test YAML | **Bootstrap** (`pipeline*.yml`) builds images and uploads **child** test pipelines. **Test** YAML (`test-*.yml`) lists pytest steps only. |
|
||||||
| Register CI YAML in `skip_ci.py` | If you add a new whitelisted test pipeline file, update `L2_YAML_FILES`, `L3_YAML_FILES`, or `L45_YAML_FILES` in `.buildkite/common/scripts/skip_ci.py` so skip-ci paths stay correct. |
|
| Register CI YAML in `skip_ci.py` | If you add a new whitelisted test pipeline file, update `L2_YAML_FILES`, `L3_YAML_FILES`, or `L45_YAML_FILES` in `.buildkite/common/scripts/skip_ci.py` so skip-ci paths stay correct. |
|
||||||
|
|
||||||
There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` prefix). Treat `.buildkite/cuda/*` as source of truth.
|
|
||||||
|
|
||||||
## Platform comparison
|
## Platform comparison
|
||||||
|
|
||||||
| Platform | Bootstrap entry | Test job files | Upload mechanism | Job hardware in YAML |
|
| Platform | Bootstrap entry | Test job files | Upload mechanism | Job hardware in YAML |
|
||||||
| -------- | ---------------- | -------------- | ---------------- | -------------------- |
|
| -------- | ---------------- | -------------- | ---------------- | -------------------- |
|
||||||
| **CUDA** | `cuda/pipeline.yml` | `test-ready.yml`, `test-merge.yml`, `test-nightly.yml`, `test-weekly.yml` | `upload_pipeline.py --upload` (expands uploader-only keys) | `mirror_hardwares: <preset>` (string) |
|
| **CUDA** | `cuda/pipeline.yml` | `test-ready.yml`, `test-merge.yml`, `test-nightly.yml`, `test-weekly.yml` | `upload_pipeline.py --upload` (expands uploader-only keys) | omit `mirror_hardwares` (from `-m`) / preset string + `MIRROR_HW` |
|
||||||
| **NPU** | `npu/pipeline-npu.yml` | `test-npu-ready.yml`, `test-npu-nightly.yml` | `upload_pipeline.py --upload` | `mirror_hardwares: a2b3_npu_1` / `a2b3_npu_4` / `a3_npu_2` |
|
| **NPU** | `npu/pipeline-npu.yml` | `test-npu-ready.yml`, `test-npu-nightly.yml` | `upload_pipeline.py --upload` | `mirror_hardwares: a2b3_npu_1` / `a2b3_npu_4` / `a3_npu_2` |
|
||||||
| **AMD** | `amd/scripts/bootstrap-amd-omni.sh` | `test-amd-ready.yml`, `test-amd-merge.yml` | Jinja (`test-template-amd-omni.j2`) → `pipeline upload` | `agent_pool` + `mirror_hardwares: [amdproduction]` (array, template filter) |
|
| **AMD** | `amd/scripts/bootstrap-amd-omni.sh` | `test-amd-ready.yml`, `test-amd-merge.yml` | Jinja (`test-template-amd-omni.j2`) → `pipeline upload` | `agent_pool` + `mirror_hardwares: [amdproduction]` (array, template filter) |
|
||||||
| **Intel** | `intel/scripts/bootstrap-intel-omni.sh` | `intel/pipeline-intel.yml` (steps inline) | Direct `pipeline upload` | Inline `agents.queue` on each step |
|
| **Intel** | `intel/scripts/bootstrap-intel-omni.sh` | `intel/pipeline-intel.yml` (steps inline) | Direct `pipeline upload` | Inline `agents.queue` on each step |
|
||||||
@@ -92,7 +90,23 @@ There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` pr
|
|||||||
|
|
||||||
**Upload:** `upload_pipeline.py --upload` expands uploader-only keys before Buildkite upload.
|
**Upload:** `upload_pipeline.py --upload` expands uploader-only keys before Buildkite upload.
|
||||||
|
|
||||||
**Hardware in YAML:** `mirror_hardwares: <preset>` (string)—preset names in [`common/ci_mirror_hardwares.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/ci_mirror_hardwares.yml). Do **not** set `agents` / `plugins` on the same step.
|
**Hardware in YAML:** omit `mirror_hardwares` to compose `{chip}_{n}` from pytest `-m` (SKU + `cards_n`). Several positive `cards_*` take the **max** n (`cards_2 and cards_3` → `{chip}_3`). `not cards_1` with no positive `cards_*` uses that chip's **highest existing** preset in `ci_mirror_hardwares.yml` (L4 → `l4_4`, H100 → `h100_4`, B200 → `b200_4`). Set a **preset string** to force a pool (marks are ignored). Do **not** set `agents` / `plugins` on the same step as `mirror_hardwares`. `MIRROR_HW=b200` remaps inferred jobs whose `-m` includes `B200`, and omits CUDA `h100_*` / `l4_*` strings. Jobs whose `-m` does not match (unset: no H100/L4; set: no B200) are **skipped**.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# inferred: H100+B200 + cards_2 → h100_2, or b200_2 when MIRROR_HW=b200
|
||||||
|
commands:
|
||||||
|
- pytest -sv tests/e2e -m "full_model and H100 and B200 and omni and cards_2"
|
||||||
|
# no mirror_hardwares
|
||||||
|
|
||||||
|
# inferred: not cards_1 (no positive cards_*) → that chip's highest pool (l4_4 / h100_4 / b200_4)
|
||||||
|
commands:
|
||||||
|
- pytest -sv tests/e2e -m "full_model and diffusion and L4 and not cards_1"
|
||||||
|
|
||||||
|
# forced preset (ignores -m SKU/cards for pool selection)
|
||||||
|
mirror_hardwares: h100_1
|
||||||
|
```
|
||||||
|
|
||||||
|
Inferred form: unset/`empty` `MIRROR_HW` matches `H100` or `L4` in `-m` (both present → H100); no match skips the step. `MIRROR_HW=b200` must appear in `-m` or the step is skipped. The uploader does **not** rewrite pytest `-m`; B200 collection comes from the YAML expression and from tests that declare `B200` in `hardware_test` / `hardware_marks` (for example `res={"cuda": ["H100", "B200"]}`). `MIRROR_HW` must be empty or `b200` (case-insensitive); unknown values (for example `b20o`) **fail the upload**. A CUDA preset string such as `mirror_hardwares: h100_4` is skipped when `MIRROR_HW=b200`. Unset `MIRROR_HW` keeps string presets. NPU presets ignore `MIRROR_HW`. A `mirror_hardwares` name that is not a key in `ci_mirror_hardwares.yml` **fails the upload**.
|
||||||
|
|
||||||
**Conventions**
|
**Conventions**
|
||||||
|
|
||||||
@@ -103,9 +117,9 @@ There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` pr
|
|||||||
|
|
||||||
**Adding a job**
|
**Adding a job**
|
||||||
|
|
||||||
1. Pick the level file (ready / merge / nightly / weekly).
|
1. Pick the level file (`test-ready.yml` / `test-merge.yml` / `test-nightly.yml` / `test-weekly.yml`).
|
||||||
2. Add a step under the right **group** (usually **E2E Test** for model pytest).
|
2. Add a step under the right **group** (usually **E2E Test** for model pytest).
|
||||||
3. Set `label`, `commands`, `mirror_hardwares`, `depends_on: upload-<level>-pipeline`.
|
3. Set `label`, `commands`, `mirror_hardwares`, `depends_on: upload-ready-pipeline` (or `upload-merge-pipeline` / `upload-nightly-pipeline` / `upload-weekly-pipeline`).
|
||||||
4. For L2/L3 E2E, add `source_file_dependencies` (pytest + model + deploy YAML prefixes).
|
4. For L2/L3 E2E, add `source_file_dependencies` (pytest + model + deploy YAML prefixes).
|
||||||
5. Dry-run:
|
5. Dry-run:
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari
|
|||||||
<a href="test_writing_guide.md#l4-level-testing-full-functionality-performance-and-documentation-testing">L4</a><br>
|
<a href="test_writing_guide.md#l4-level-testing-full-functionality-performance-and-documentation-testing">L4</a><br>
|
||||||
L4: Purpose, Test Content, Directory Location, Example
|
L4: Purpose, Test Content, Directory Location, Example
|
||||||
</td>
|
</td>
|
||||||
<td>Nightly</td>
|
<td>Nightly / Days before Release<br>(additional GPU SKUs)</td>
|
||||||
<td>GPU</td>
|
<td>GPU</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -175,6 +175,15 @@ Before entering specific testing levels, the project establishes two common spec
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
### L4 cadence (nightly and pre-release)
|
||||||
|
|
||||||
|
CUDA **L4** (`full_model`) jobs are defined once in [`.buildkite/cuda/test-nightly.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-nightly.yml). That file is reused at two cadences:
|
||||||
|
|
||||||
|
- **Nightly:** scheduled `main` builds with `NIGHTLY=1` (and PR labels such as `nightly-test` / `omni-test`). Default fleet is typically H100.
|
||||||
|
- **Pre-release:** the same L4 suite runs again on extra GPU SKUs before a release (for example B200 via `MIRROR_HW=b200`), so `full_model` coverage is not limited to the nightly machine type.
|
||||||
|
|
||||||
|
How count-form `mirror_hardwares` and `MIRROR_HW` pick a SKU: [CI Settings](./ci_settings.md).
|
||||||
|
|
||||||
### L2 / L3 diff-aware CI (CUDA)
|
### L2 / L3 diff-aware CI (CUDA)
|
||||||
|
|
||||||
On CUDA **L2** and **L3**, **E2E Test** Buildkite jobs may be omitted at pipeline upload when the PR diff does not touch their path prefixes; other groups still always upload. Full two-layer mechanics and YAML examples: [CI Settings — Diff-aware CI](./ci_settings.md#diff-aware-ci) ([step filtering](./ci_settings.md#step-filtering)).
|
On CUDA **L2** and **L3**, **E2E Test** Buildkite jobs may be omitted at pipeline upload when the PR diff does not touch their path prefixes; other groups still always upload. Full two-layer mechanics and YAML examples: [CI Settings — Diff-aware CI](./ci_settings.md#diff-aware-ci) ([step filtering](./ci_settings.md#step-filtering)).
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ vllm-omni serve <model> --diffusion-attention-backend TORCH_SDPA
|
|||||||
## `FLASH_ATTN`
|
## `FLASH_ATTN`
|
||||||
|
|
||||||
`FLASH_ATTN` uses the installed FlashAttention implementation. On Blackwell it
|
`FLASH_ATTN` uses the installed FlashAttention implementation. On Blackwell it
|
||||||
prefers the CuTe-based FlashAttention 4 path and falls back to
|
is FA4 only (`flash_attn.cute` / `vllm-omni[fa4]`). If FA4 is unavailable,
|
||||||
FlashAttention 3 or 2 when that path is unavailable. On Hopper, Ada, and
|
explicit `FLASH_ATTN` is rejected and automatic selection continues to another
|
||||||
Ampere it is the preferred automatic route when a compatible package is
|
compatible Blackwell backend. Hopper-only FA2/FA3 wheels are not used, even if
|
||||||
installed.
|
they import. On Hopper, Ada, and Ampere it is the preferred automatic route
|
||||||
|
when a compatible package is installed.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
vllm-omni serve <model> --diffusion-attention-backend FLASH_ATTN
|
vllm-omni serve <model> --diffusion-attention-backend FLASH_ATTN
|
||||||
@@ -39,8 +40,10 @@ pip install 'vllm-omni[fa4]'
|
|||||||
```
|
```
|
||||||
|
|
||||||
Version `4.0.0b18` is required; earlier beta wheels had known JIT failures on
|
Version `4.0.0b18` is required; earlier beta wheels had known JIT failures on
|
||||||
Blackwell. If the CuTe path is unavailable, the backend falls back to the
|
Blackwell. On Blackwell, `FLASH_ATTN` is FA4 only: Hopper-only FA2/FA3
|
||||||
compatible FlashAttention 3 or 2 path.
|
wheels are not used, even if they import. If FA4 is missing, explicit
|
||||||
|
`FLASH_ATTN` is rejected and automatic selection continues to other
|
||||||
|
Blackwell backends.
|
||||||
|
|
||||||
## `TRTLLM_ATTN`
|
## `TRTLLM_ATTN`
|
||||||
|
|
||||||
@@ -89,6 +92,13 @@ DIFFUSION_ATTENTION_BACKEND=FLASHINFER_ATTN \
|
|||||||
option on CUDA platforms and an automatic Blackwell fallback when FlashInfer
|
option on CUDA platforms and an automatic Blackwell fallback when FlashInfer
|
||||||
is installed but cuDNN is too old for `CUDNN_ATTN`.
|
is installed but cuDNN is too old for `CUDNN_ATTN`.
|
||||||
|
|
||||||
|
On Blackwell, `auto` resolves to FlashInfer cute-dsl, which cannot run a
|
||||||
|
nontrivial custom mask. Automatic selection may fall back to SDPA for those
|
||||||
|
masks. An explicit `FLASHINFER_ATTN` selection does not: sequence-parallel
|
||||||
|
auto-padding that would create a padding mask is rejected at capability
|
||||||
|
preflight. Use `TORCH_SDPA`, pin `quant.flashinfer_backend` to `fa2`/`fa3`
|
||||||
|
where those kernels exist, or choose a mask-capable backend.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
vllm-omni serve <model> --diffusion-attention-backend FLASHINFER_ATTN
|
vllm-omni serve <model> --diffusion-attention-backend FLASHINFER_ATTN
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ implementations to reduce numerical drift and sampling divergence.
|
|||||||
pip install kernels==0.14.1
|
pip install kernels==0.14.1
|
||||||
```
|
```
|
||||||
|
|
||||||
If `kernels` is unavailable, vLLM-Omni warns and falls back to the
|
If `kernels` is unavailable, an explicit `FLASH_ATTN_HUB` or
|
||||||
corresponding local FlashAttention implementation. On CUDA devices older than
|
`FLASH_ATTN_3_HUB` selection raises. Automatic / `FLASH_ATTN` selection in the
|
||||||
Hopper, `FLASH_ATTN_3_HUB` falls back to `FLASH_ATTN_HUB`.
|
Diffusers adapter may still walk hub then local FlashAttention backends.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -46,20 +46,20 @@ More failure modes (for example, network jitter and network interruption) will b
|
|||||||
## Fault Injection Scenario Matrix
|
## Fault Injection Scenario Matrix
|
||||||
|
|
||||||
| Scenario | Fault Type | System Behavior | Current Status |
|
| Scenario | Fault Type | System Behavior | Current Status |
|
||||||
|------|----------------------------|----------|----------|
|
| ------ | ---------------------------- | ---------- | ---------- |
|
||||||
| No load | Send `SIGKILL` to Worker | Worker process is killed immediately; main service detects child-process loss and turns unavailable; API enters stable 5xx | |
|
| No load | Send `SIGKILL` to Worker | Worker process is killed immediately; main service detects child-process loss and turns unavailable; API enters stable 5xx | |
|
||||||
| No load | Send `SIGTERM` to Worker | Worker exits after receiving termination signal; main service is marked unavailable; API enters stable 5xx | |
|
| No load | Send `SIGTERM` to Worker | Worker exits after receiving termination signal; main service is marked unavailable; API enters stable 5xx | |
|
||||||
| No load | Send `SIGKILL` to serve main process | serve main process exits instantly; request connections are interrupted; related child processes are cleaned up with no residue; GPU memory is released quickly | [#3725](https://github.com/vllm-project/vllm-omni/issues/3725) <br>[#43060](https://github.com/vllm-project/vllm/issues/43060) |
|
| No load | Send `SIGKILL` to serve main process | serve main process exits instantly; request connections are interrupted; related child processes are cleaned up with no residue; GPU memory is released quickly | [#3725](https://github.com/vllm-project/vllm-omni/issues/3725) <br>[#43060](https://github.com/vllm-project/vllm/issues/43060) |
|
||||||
| No load | Send `SIGTERM` to serve main process | serve enters graceful shutdown and stops serving; then exits and completes cleanup; GPU memory is released | |
|
| No load | Send `SIGTERM` to serve main process | serve enters graceful shutdown and stops serving; then exits and completes cleanup; GPU memory is released | |
|
||||||
| No load | Send `SIGINT` to serve main process (equivalent to `Ctrl+C`) | Triggers serve shutdown path; service stops responding and becomes unavailable; related child processes exit and resources are released | |
|
| No load | Send `SIGINT` to serve main process (equivalent to `Ctrl+C`) | Triggers serve shutdown path; service stops responding and becomes unavailable; related child processes exit and resources are released | |
|
||||||
| No load | Send `SIGKILL` to all related processes | All related processes terminate immediately; service becomes unavailable at once; no residual processes remain; GPU memory is released quickly | |
|
| No load | Send `SIGKILL` to all related processes | All related processes terminate immediately; service becomes unavailable at once; no residual processes remain; GPU memory is released quickly | |
|
||||||
| No load | Send `SIGTERM` to all related processes | All processes enter exit flow and complete shutdown; service becomes unavailable; resource release is completed | |
|
| No load | Send `SIGTERM` to all related processes | All processes enter exit flow and complete shutdown; service becomes unavailable; resource release is completed | |
|
||||||
| Under load | Send `SIGKILL` to Worker | In-flight requests are hard interrupted (5xx/connection drop); main service becomes unavailable | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
| Under load | Send `SIGKILL` to Worker | In-flight requests are hard interrupted (5xx/connection drop); main service becomes unavailable | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
||||||
| Under load | Send `SIGTERM` to Worker | In-flight requests are canceled or fail fast; main service becomes unavailable | |
|
| Under load | Send `SIGTERM` to Worker | In-flight requests are canceled or fail fast; main service becomes unavailable | |
|
||||||
| Under load | Send `SIGKILL` to serve main process | serve is hard-killed and current connections are interrupted; in-flight requests fail; after cleanup there are no residual processes and GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
| Under load | Send `SIGKILL` to serve main process | serve is hard-killed and current connections are interrupted; in-flight requests fail; after cleanup there are no residual processes and GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
||||||
| Under load | Send `SIGTERM` to serve main process | serve stops accepting new requests and executes shutdown flow; in-flight requests fail; no residue remains and GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
| Under load | Send `SIGTERM` to serve main process | serve stops accepting new requests and executes shutdown flow; in-flight requests fail; no residue remains and GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
||||||
| Under load | Send `SIGINT` to serve main process (equivalent to `Ctrl+C`) | `Ctrl+C`-style serve shutdown; in-flight requests fail (5xx/connection interruption); service unavailable; after exit there is no residue and GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
| Under load | Send `SIGINT` to serve main process (equivalent to `Ctrl+C`) | `Ctrl+C`-style serve shutdown; in-flight requests fail (5xx/connection interruption); service unavailable; after exit there is no residue and GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
||||||
| Under load | Send `SIGKILL` to all related processes | All processes terminate instantly; all in-flight requests fail; service becomes unavailable immediately; GPU memory is released quickly | |
|
| Under load | Send `SIGKILL` to all related processes | All processes terminate instantly; all in-flight requests fail; service becomes unavailable immediately; GPU memory is released quickly | |
|
||||||
| Under load | Send `SIGTERM` to all related processes | All processes exit gracefully; in-flight requests fail; service unavailable; GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
| Under load | Send `SIGTERM` to all related processes | All processes exit gracefully; in-flight requests fail; service unavailable; GPU memory is released | [#3683](https://github.com/vllm-project/vllm-omni/issues/3683) |
|
||||||
| OOM | Occupy all free GPU memory via an extra process | After OOM injection process starts, GPU memory is continuously saturated; service enters unavailable/degraded state and health check drops to 503; different request types (chat/speech, etc.) fail fast within a fixed time and return 500 (no hanging) | [#4285](https://github.com/vllm-project/vllm-omni/issues/4285) |
|
| OOM | Occupy all free GPU memory via an extra process | After OOM injection process starts, GPU memory is continuously saturated; service enters unavailable/degraded state and health check drops to 503; different request types (chat/speech, etc.) fail fast within a fixed time and return 500 (no hanging) | [#4285](https://github.com/vllm-project/vllm-omni/issues/4285) |
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / ".buildkite" / "com
|
|||||||
from skip_ci import resolve_ci_decision # noqa: E402
|
from skip_ci import resolve_ci_decision # noqa: E402
|
||||||
from upload_pipeline import ( # noqa: E402
|
from upload_pipeline import ( # noqa: E402
|
||||||
_expand_mirror_hardwares,
|
_expand_mirror_hardwares,
|
||||||
|
_get_mirror_hw_selector,
|
||||||
_load_bootstrap_steps,
|
_load_bootstrap_steps,
|
||||||
_render_bootstrap_pipeline,
|
_render_bootstrap_pipeline,
|
||||||
_render_test_pipeline,
|
_render_test_pipeline,
|
||||||
@@ -116,7 +117,8 @@ def test_yaml_gated_l2_still_enables_image_via_ready_base() -> None:
|
|||||||
assert "if: true" not in rendered
|
assert "if: true" not in rendered
|
||||||
|
|
||||||
|
|
||||||
def test_mirror_hardwares_l4_1_expands_to_agents_and_plugins() -> None:
|
def test_mirror_hardwares_l4_1_expands_to_agents_and_plugins(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr("upload_pipeline._get_mirror_hw_selector", lambda: "")
|
||||||
doc = {
|
doc = {
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
@@ -182,6 +184,156 @@ def test_mirror_hardwares_a2b3_npu_4_expands_agents_image_and_plugins() -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _gpu_limit(step: dict) -> int:
|
||||||
|
return step["plugins"][0]["kubernetes"]["podSpec"]["containers"][0]["resources"]["limits"]["nvidia.com/gpu"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mirror_hardwares_mapping_is_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError, match="must be a preset string"):
|
||||||
|
_expand_mirror_hardwares(
|
||||||
|
{"label": "bad", "mirror_hardwares": {"default": "h100_2", "b200": "b200_2"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("hardware", [2, "2", "h100_99", "not_a_preset"])
|
||||||
|
def test_mirror_hardwares_unknown_name_is_rejected(hardware: int | str) -> None:
|
||||||
|
with pytest.raises(ValueError, match="unknown mirror_hardwares"):
|
||||||
|
_expand_mirror_hardwares({"label": "unknown preset", "mirror_hardwares": hardware})
|
||||||
|
|
||||||
|
|
||||||
|
def test_mirror_hardwares_string_ignores_pytest_cards_mark(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr("upload_pipeline._get_mirror_hw_selector", lambda: "")
|
||||||
|
step = _expand_mirror_hardwares(
|
||||||
|
{
|
||||||
|
"label": "forced",
|
||||||
|
"commands": ['pytest -sv tests/e2e -m "full_model and L4 and cards_4"'],
|
||||||
|
"mirror_hardwares": "h100_1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert step is not None
|
||||||
|
assert step["agents"]["queue"] == "mithril-h100-pool"
|
||||||
|
assert _gpu_limit(step) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_mirror_hardwares_b200_omits_cuda_strings_and_remaps_inferred(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("upload_pipeline._get_mirror_hw_selector", lambda: "b200")
|
||||||
|
assert _expand_mirror_hardwares({"label": "h100", "mirror_hardwares": "h100_4"}) is None
|
||||||
|
assert _expand_mirror_hardwares({"label": "l4", "mirror_hardwares": "l4_1"}) is None
|
||||||
|
b200 = _expand_mirror_hardwares({"label": "b200", "mirror_hardwares": "b200_2"})
|
||||||
|
assert b200 is not None and b200["agents"]["queue"] == "b200-k8s"
|
||||||
|
npu = _expand_mirror_hardwares({"label": "npu", "mirror_hardwares": "a2b3_npu_4"})
|
||||||
|
assert npu is not None and npu["agents"]["queue"] == "ascend-a2b3"
|
||||||
|
|
||||||
|
rendered = _render_test_pipeline(
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"group": ":card_index_dividers: Mixed",
|
||||||
|
"steps": [
|
||||||
|
{"label": "H100 only", "mirror_hardwares": "h100_4"},
|
||||||
|
{
|
||||||
|
"label": "Count remap",
|
||||||
|
"commands": [
|
||||||
|
'pytest -sv tests/e2e -m "full_model and H100 and B200 and omni and cards_2"',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": ":card_index_dividers: H100 only group",
|
||||||
|
"steps": [{"label": "Skip me", "mirror_hardwares": "h100_1"}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changed_files=None,
|
||||||
|
)
|
||||||
|
groups = [step.get("group") for step in rendered["steps"]]
|
||||||
|
assert ":card_index_dividers: H100 only group" not in groups
|
||||||
|
mixed = next(step for step in rendered["steps"] if step.get("group") == ":card_index_dividers: Mixed")
|
||||||
|
assert [child["label"] for child in mixed["steps"]] == ["Count remap"]
|
||||||
|
assert mixed["steps"][0]["agents"]["queue"] == "b200-k8s"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("selector", "expr", "queue", "gpus"),
|
||||||
|
[
|
||||||
|
("", "H100 and B200 and cards_2", "mithril-h100-pool", 2),
|
||||||
|
("b200", "H100 and B200 and cards_2", "b200-k8s", 2),
|
||||||
|
("", "H100 or L4 and cards_4", "mithril-h100-pool", 4),
|
||||||
|
("", "L4 and B200 and cards_4", "l4-k8s", 4),
|
||||||
|
("", "H100 and cards_2 and cards_3", "mithril-h100-pool", 3),
|
||||||
|
("", "H100 and not cards_1", "mithril-h100-pool", 4),
|
||||||
|
("b200", "H100 and B200 and not cards_1", "b200-k8s", 4),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_mirror_hardwares_inferred_from_marks(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
selector: str,
|
||||||
|
expr: str,
|
||||||
|
queue: str,
|
||||||
|
gpus: int,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("upload_pipeline._get_mirror_hw_selector", lambda: selector)
|
||||||
|
step = _expand_mirror_hardwares({"label": "job", "commands": [f'pytest -sv tests/e2e -m "{expr}"']})
|
||||||
|
assert step is not None
|
||||||
|
assert step["agents"]["queue"] == queue
|
||||||
|
assert _gpu_limit(step) == gpus
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("selector", "expr"),
|
||||||
|
[
|
||||||
|
("b200", "H100 and cards_2"),
|
||||||
|
("", "B200 and cards_2"),
|
||||||
|
("", "full_model and cards_2"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_mirror_hardwares_inferred_skips_unmatched_chip(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
selector: str,
|
||||||
|
expr: str,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("upload_pipeline._get_mirror_hw_selector", lambda: selector)
|
||||||
|
assert _expand_mirror_hardwares({"label": "job", "commands": [f'pytest -sv tests/e2e -m "{expr}"']}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_mirror_hardwares_inferred_missing_preset_is_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr("upload_pipeline._get_mirror_hw_selector", lambda: "")
|
||||||
|
with pytest.raises(ValueError, match="has no preset 'h100_8'"):
|
||||||
|
_expand_mirror_hardwares(
|
||||||
|
{"label": "H100 8-gpu", "commands": ['pytest -sv tests/e2e -m "H100 and cards_8"']},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_step_without_mirror_hardwares_is_unchanged() -> None:
|
||||||
|
step = {"label": "CPU report", "commands": ["echo ok"], "agents": {"queue": "cpu_queue_premerge"}}
|
||||||
|
assert _expand_mirror_hardwares(step) is step
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("raw", "expected"), [("", ""), (" ", ""), ("b200", "b200"), ("B200", "b200")])
|
||||||
|
def test_mirror_hw_selector_empty_or_b200(monkeypatch: pytest.MonkeyPatch, raw: str, expected: str) -> None:
|
||||||
|
monkeypatch.setenv("MIRROR_HW", raw)
|
||||||
|
assert _get_mirror_hw_selector() == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_mirror_hw_typo_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("MIRROR_HW", "b20o")
|
||||||
|
with pytest.raises(ValueError, match=r"unsupported MIRROR_HW='b20o'"):
|
||||||
|
_get_mirror_hw_selector()
|
||||||
|
with pytest.raises(ValueError, match=r"unsupported MIRROR_HW='b20o'"):
|
||||||
|
_render_test_pipeline(
|
||||||
|
{
|
||||||
|
"steps": [
|
||||||
|
{"label": "CPU report", "commands": ["echo ok"]},
|
||||||
|
{"label": "H100 string", "mirror_hardwares": "h100_4"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changed_files=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _surviving_labels(doc: dict, changed_files: list[str]) -> set[str]:
|
def _surviving_labels(doc: dict, changed_files: list[str]) -> set[str]:
|
||||||
rendered = _render_test_pipeline(doc, changed_files=changed_files)
|
rendered = _render_test_pipeline(doc, changed_files=changed_files)
|
||||||
labels: set[str] = set()
|
labels: set[str] = set()
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ from tests.dfx.conftest import (
|
|||||||
)
|
)
|
||||||
from tests.helpers.runtime import get_open_port
|
from tests.helpers.runtime import get_open_port
|
||||||
|
|
||||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
|
||||||
os.environ.setdefault("DIFFUSION_ATTENTION_BACKEND", "FLASH_ATTN")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Inline field processing
|
# Inline field processing
|
||||||
@@ -837,7 +834,7 @@ def run_benchmark(
|
|||||||
"Quantization": _to_quantization_value(server_type, serve_args_dict),
|
"Quantization": _to_quantization_value(server_type, serve_args_dict),
|
||||||
"offload": _to_offload_string(server_type, serve_args_dict),
|
"offload": _to_offload_string(server_type, serve_args_dict),
|
||||||
"compile": _to_compile_value(server_type, serve_args_dict),
|
"compile": _to_compile_value(server_type, serve_args_dict),
|
||||||
"Attn_backend": os.environ.get("DIFFUSION_ATTENTION_BACKEND", ""),
|
"Attn_backend": str(serve_args_dict.get("diffusion-attention-backend") or ""),
|
||||||
"num_inference_steps": params.get("num-inference-steps", ""),
|
"num_inference_steps": params.get("num-inference-steps", ""),
|
||||||
"completed": completed,
|
"completed": completed,
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"vae-patch-parallel-size": 4,
|
"vae-patch-parallel-size": 4,
|
||||||
"vae-parallel-mode": "tile",
|
"vae-parallel-mode": "tile",
|
||||||
"vae-use-tiling": true,
|
"vae-use-tiling": true,
|
||||||
"diffusion-attention-backend": "FLASH_ATTN",
|
|
||||||
"stage-init-timeout": 1800,
|
"stage-init-timeout": 1800,
|
||||||
"init-timeout": 1800,
|
"init-timeout": 1800,
|
||||||
"enable-diffusion-pipeline-profiler": true
|
"enable-diffusion-pipeline-profiler": true
|
||||||
@@ -102,7 +101,6 @@
|
|||||||
"vae-patch-parallel-size": 4,
|
"vae-patch-parallel-size": 4,
|
||||||
"vae-parallel-mode": "tile",
|
"vae-parallel-mode": "tile",
|
||||||
"vae-use-tiling": true,
|
"vae-use-tiling": true,
|
||||||
"diffusion-attention-backend": "FLASH_ATTN",
|
|
||||||
"stage-init-timeout": 1800,
|
"stage-init-timeout": 1800,
|
||||||
"init-timeout": 1800,
|
"init-timeout": 1800,
|
||||||
"enable-diffusion-pipeline-profiler": true
|
"enable-diffusion-pipeline-profiler": true
|
||||||
@@ -175,7 +173,6 @@
|
|||||||
"vae-patch-parallel-size": 4,
|
"vae-patch-parallel-size": 4,
|
||||||
"vae-parallel-mode": "tile",
|
"vae-parallel-mode": "tile",
|
||||||
"vae-use-tiling": true,
|
"vae-use-tiling": true,
|
||||||
"diffusion-attention-backend": "FLASH_ATTN",
|
|
||||||
"stage-init-timeout": 1800,
|
"stage-init-timeout": 1800,
|
||||||
"init-timeout": 1800,
|
"init-timeout": 1800,
|
||||||
"enable-diffusion-pipeline-profiler": true
|
"enable-diffusion-pipeline-profiler": true
|
||||||
@@ -247,7 +244,6 @@
|
|||||||
"vae-patch-parallel-size": 4,
|
"vae-patch-parallel-size": 4,
|
||||||
"vae-parallel-mode": "tile",
|
"vae-parallel-mode": "tile",
|
||||||
"vae-use-tiling": true,
|
"vae-use-tiling": true,
|
||||||
"diffusion-attention-backend": "FLASH_ATTN",
|
|
||||||
"stage-init-timeout": 1800,
|
"stage-init-timeout": 1800,
|
||||||
"init-timeout": 1800,
|
"init-timeout": 1800,
|
||||||
"enable-diffusion-pipeline-profiler": true,
|
"enable-diffusion-pipeline-profiler": true,
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ def test_create_unique_server_pytest_params_applies_marks(tmp_path):
|
|||||||
assert len(by_id["test_with_mark"].values) == 1
|
assert len(by_id["test_with_mark"].values) == 1
|
||||||
assert isinstance(by_id["test_with_mark"].values[0], tuple)
|
assert isinstance(by_id["test_with_mark"].values[0], tuple)
|
||||||
assert any(m.name == "H100" for m in by_id["test_with_mark"].marks)
|
assert any(m.name == "H100" for m in by_id["test_with_mark"].marks)
|
||||||
|
assert not any(m.name == "B200" for m in by_id["test_with_mark"].marks)
|
||||||
assert not any(m.name == "H100" for m in by_id["test_without_mark"].marks)
|
assert not any(m.name == "H100" for m in by_id["test_without_mark"].marks)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import torch
|
|||||||
|
|
||||||
import vllm_omni.diffusion.attention.layer as layer_mod
|
import vllm_omni.diffusion.attention.layer as layer_mod
|
||||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||||
|
from vllm_omni.diffusion.attention.backends.flash_attn import FlashAttentionBackend
|
||||||
from vllm_omni.diffusion.attention.layer import Attention
|
from vllm_omni.diffusion.attention.layer import Attention
|
||||||
from vllm_omni.diffusion.config import (
|
from vllm_omni.diffusion.config import (
|
||||||
get_current_diffusion_config,
|
get_current_diffusion_config,
|
||||||
@@ -73,6 +74,12 @@ class TestAttentionSpec:
|
|||||||
assert bk["dtype_vo"] == "fp8_e4m3"
|
assert bk["dtype_vo"] == "fp8_e4m3"
|
||||||
assert bk["flashinfer_backend"] == "trtllm-gen"
|
assert bk["flashinfer_backend"] == "trtllm-gen"
|
||||||
|
|
||||||
|
def test_quant_flashinfer_backend_serialized_without_dtype(self):
|
||||||
|
spec = AttentionSpec(backend="FLASHINFER_ATTN", quant={"flashinfer_backend": "fa2"})
|
||||||
|
assert spec.quant is not None
|
||||||
|
assert spec.quant.enabled is True
|
||||||
|
assert spec.backend_kwargs() == {"quant": {"flashinfer_backend": "fa2"}}
|
||||||
|
|
||||||
def test_quant_and_skip_softmax_coexist(self):
|
def test_quant_and_skip_softmax_coexist(self):
|
||||||
bk = AttentionSpec(
|
bk = AttentionSpec(
|
||||||
backend="TRTLLM_ATTN", quant={"dtype_qk": "int8"}, skip_softmax={"target_sparsity": 0.5}
|
backend="TRTLLM_ATTN", quant={"dtype_qk": "int8"}, skip_softmax={"target_sparsity": 0.5}
|
||||||
@@ -150,8 +157,8 @@ class TestAttentionSpec:
|
|||||||
def test_block_sparse_precision_enum_input_normalized(self):
|
def test_block_sparse_precision_enum_input_normalized(self):
|
||||||
"""RainFusionPrecision enum members must normalize to their string value.
|
"""RainFusionPrecision enum members must normalize to their string value.
|
||||||
|
|
||||||
Regression: the str,Enum members passed the value check but str() later
|
Regression: ``str(str, Enum)`` later produced ``RainFusionPrecision.MIX``
|
||||||
produced "RainFusionPrecision.MIX" instead of "mix" (StrEnum fixes it).
|
instead of ``mix``. ``BlockSparseSpec`` stores ``member.value``.
|
||||||
"""
|
"""
|
||||||
from vllm_omni.diffusion.attention.backends.rainfusion_attn import RainFusionConfig
|
from vllm_omni.diffusion.attention.backends.rainfusion_attn import RainFusionConfig
|
||||||
from vllm_omni.diffusion.data import RainFusionPrecision
|
from vllm_omni.diffusion.data import RainFusionPrecision
|
||||||
@@ -509,6 +516,49 @@ class TestAttentionInitUsesCurrentDiffusionConfig:
|
|||||||
dense_flash.assert_not_called()
|
dense_flash.assert_not_called()
|
||||||
sdpa.assert_called_once()
|
sdpa.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fa4_available", [False, True])
|
||||||
|
def test_paged_cuda_memory_profile_uses_sdpa_without_dense_fa4(self, monkeypatch, fa4_available):
|
||||||
|
attention = Attention.__new__(Attention)
|
||||||
|
attention._scheduler_paged_kv = True
|
||||||
|
attention.paged_kv_cache_role = "primary"
|
||||||
|
attention.backend_pref = "FLASH_ATTN"
|
||||||
|
attention.attn_backend = SimpleNamespace(supports_piecewise_spans=True, get_name=lambda: "FLASH_ATTN")
|
||||||
|
flash_output = torch.ones(1)
|
||||||
|
sdpa_output = torch.zeros(1)
|
||||||
|
dense_flash = Mock(return_value=flash_output)
|
||||||
|
sdpa = Mock(return_value=sdpa_output)
|
||||||
|
attention.attention = SimpleNamespace(forward=dense_flash)
|
||||||
|
attention.sdpa_fallback = SimpleNamespace(forward=sdpa)
|
||||||
|
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
layer_mod,
|
||||||
|
"get_forward_context",
|
||||||
|
lambda: SimpleNamespace(in_diffusion_kv_memory_profile=True),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(layer_mod.current_omni_platform, "is_npu", lambda: False)
|
||||||
|
monkeypatch.setattr(layer_mod.current_omni_platform, "is_cuda", lambda: True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
layer_mod.current_omni_platform,
|
||||||
|
"supports_diffusion_dense_flash_attention",
|
||||||
|
lambda: fa4_available,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = attention._run_local_attention(
|
||||||
|
torch.zeros(1, dtype=torch.bfloat16),
|
||||||
|
torch.zeros(1, dtype=torch.bfloat16),
|
||||||
|
torch.zeros(1, dtype=torch.bfloat16),
|
||||||
|
AttentionMetadata(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if fa4_available:
|
||||||
|
assert result is flash_output
|
||||||
|
dense_flash.assert_called_once()
|
||||||
|
sdpa.assert_not_called()
|
||||||
|
else:
|
||||||
|
assert result is sdpa_output
|
||||||
|
dense_flash.assert_not_called()
|
||||||
|
sdpa.assert_called_once()
|
||||||
|
|
||||||
def test_dense_flash_import_error_is_not_reclassified(self, monkeypatch):
|
def test_dense_flash_import_error_is_not_reclassified(self, monkeypatch):
|
||||||
attention = Attention.__new__(Attention)
|
attention = Attention.__new__(Attention)
|
||||||
attention._scheduler_paged_kv = False
|
attention._scheduler_paged_kv = False
|
||||||
@@ -579,7 +629,106 @@ class TestAttentionInitUsesCurrentDiffusionConfig:
|
|||||||
|
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
assert attention.attn_backend is _DenseBackend
|
assert attention.attn_backend is _DenseBackend
|
||||||
assert attention.backend_pref is None
|
# Auto path records the resolved backend name for Ring/SP; it is not explicit.
|
||||||
|
assert attention.backend_pref == _DenseBackend.get_name()
|
||||||
|
assert attention.backend_explicit is False
|
||||||
|
|
||||||
|
def test_internal_paged_selector_does_not_require_dense_fa4(self, monkeypatch):
|
||||||
|
"""Blackwell CUDNN default + marked paged layer must not act as explicit FLASH_ATTN."""
|
||||||
|
|
||||||
|
class _FakeAttentionImpl:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def forward(self, query, key, value, attn_metadata=None):
|
||||||
|
return query
|
||||||
|
|
||||||
|
class _CuDNNBackend:
|
||||||
|
supports_paged_kv = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "CUDNN_ATTN"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls():
|
||||||
|
return _FakeAttentionImpl
|
||||||
|
|
||||||
|
def _fake_get_attn_backend_for_role(*args, attention_config=None, **kwargs):
|
||||||
|
if attention_config is not None:
|
||||||
|
spec, _ = attention_config.resolve_with_source(role="self", role_category=None)
|
||||||
|
if spec is not None and spec.backend.upper() == "FLASH_ATTN":
|
||||||
|
raise ValueError(
|
||||||
|
"FLASH_ATTN was explicitly selected but is unsupported "
|
||||||
|
"(Blackwell requires CuTe FlashAttention-4 "
|
||||||
|
"(flash_attn.cute / vllm-omni[fa4]); FA2/FA3 kernels are Hopper-only). "
|
||||||
|
"Select a compatible backend."
|
||||||
|
)
|
||||||
|
return _CuDNNBackend, None
|
||||||
|
|
||||||
|
monkeypatch.setattr(layer_mod, "get_attn_backend_for_role", _fake_get_attn_backend_for_role)
|
||||||
|
monkeypatch.setattr(layer_mod.SDPABackend, "get_impl_cls", staticmethod(lambda: _FakeAttentionImpl))
|
||||||
|
monkeypatch.setattr(FlashAttentionBackend, "get_impl_cls", staticmethod(lambda: _FakeAttentionImpl))
|
||||||
|
monkeypatch.setattr(layer_mod, "build_parallel_attention_strategy", lambda **kwargs: object())
|
||||||
|
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: False)
|
||||||
|
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(),
|
||||||
|
diffusion_kv_mode=layer_mod.DiffusionKVCacheMode.PAGED_SCHEDULER,
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1),
|
||||||
|
diffusion_kv_cache_dtype=None,
|
||||||
|
diffusion_kv_cache_skip_step_indices=None,
|
||||||
|
diffusion_kv_cache_skip_layer_indices=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
attention = Attention(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
causal=False,
|
||||||
|
softmax_scale=1.0,
|
||||||
|
paged_kv_cache_role="primary",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert attention.attn_backend is FlashAttentionBackend
|
||||||
|
assert attention.backend_pref == "FLASH_ATTN"
|
||||||
|
assert attention.backend_explicit is False
|
||||||
|
assert attention.attn_spec is None
|
||||||
|
|
||||||
|
def test_user_explicit_flash_attn_still_requires_dense_fa4(self, monkeypatch):
|
||||||
|
def _fake_get_attn_backend_for_role(*args, attention_config=None, **kwargs):
|
||||||
|
if attention_config is not None:
|
||||||
|
spec, _ = attention_config.resolve_with_source(role="self", role_category=None)
|
||||||
|
if spec is not None and spec.backend.upper() == "FLASH_ATTN":
|
||||||
|
raise ValueError(
|
||||||
|
"FLASH_ATTN was explicitly selected but is unsupported "
|
||||||
|
"(Blackwell requires CuTe FlashAttention-4 "
|
||||||
|
"(flash_attn.cute / vllm-omni[fa4]); FA2/FA3 kernels are Hopper-only). "
|
||||||
|
"Select a compatible backend."
|
||||||
|
)
|
||||||
|
raise AssertionError("explicit FLASH_ATTN must not fall through to the platform default")
|
||||||
|
|
||||||
|
monkeypatch.setattr(layer_mod, "get_attn_backend_for_role", _fake_get_attn_backend_for_role)
|
||||||
|
monkeypatch.setattr(layer_mod, "build_parallel_attention_strategy", lambda **kwargs: object())
|
||||||
|
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: False)
|
||||||
|
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(default=AttentionSpec(backend="FLASH_ATTN")),
|
||||||
|
diffusion_kv_mode=layer_mod.DiffusionKVCacheMode.PAGED_SCHEDULER,
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1),
|
||||||
|
diffusion_kv_cache_dtype=None,
|
||||||
|
diffusion_kv_cache_skip_step_indices=None,
|
||||||
|
diffusion_kv_cache_skip_layer_indices=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with set_current_diffusion_config(od_config), pytest.raises(ValueError, match="explicitly selected"):
|
||||||
|
Attention(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
causal=False,
|
||||||
|
softmax_scale=1.0,
|
||||||
|
paged_kv_cache_role="primary",
|
||||||
|
)
|
||||||
|
|
||||||
def test_attention_init_uses_current_diffusion_config_without_forward_context(self, monkeypatch):
|
def test_attention_init_uses_current_diffusion_config_without_forward_context(self, monkeypatch):
|
||||||
class _FakeAttentionImpl:
|
class _FakeAttentionImpl:
|
||||||
@@ -614,9 +763,10 @@ class TestAttentionInitUsesCurrentDiffusionConfig:
|
|||||||
return _FakeBackend, AttentionSpec(backend="TRTLLM_ATTN", skip_softmax={"target_sparsity": 0.5})
|
return _FakeBackend, AttentionSpec(backend="TRTLLM_ATTN", skip_softmax={"target_sparsity": 0.5})
|
||||||
|
|
||||||
class _FakeRingParallelAttention:
|
class _FakeRingParallelAttention:
|
||||||
def __init__(self, sp_group, attn_backend_pref=None):
|
def __init__(self, sp_group, attn_backend_pref=None, attn_backend_explicit=False):
|
||||||
self.sp_group = sp_group
|
self.sp_group = sp_group
|
||||||
self.attn_backend_pref = attn_backend_pref
|
self.attn_backend_pref = attn_backend_pref
|
||||||
|
self.attn_backend_explicit = attn_backend_explicit
|
||||||
|
|
||||||
monkeypatch.setattr(layer_mod, "get_attn_backend_for_role", _fake_get_attn_backend_for_role)
|
monkeypatch.setattr(layer_mod, "get_attn_backend_for_role", _fake_get_attn_backend_for_role)
|
||||||
monkeypatch.setattr(layer_mod.SDPABackend, "get_impl_cls", staticmethod(lambda: _FakeAttentionImpl))
|
monkeypatch.setattr(layer_mod.SDPABackend, "get_impl_cls", staticmethod(lambda: _FakeAttentionImpl))
|
||||||
@@ -662,11 +812,134 @@ class TestAttentionInitUsesCurrentDiffusionConfig:
|
|||||||
assert attn.use_ring is True
|
assert attn.use_ring is True
|
||||||
assert attn.ring_runner is not None
|
assert attn.ring_runner is not None
|
||||||
assert attn.ring_runner.attn_backend_pref == "TRTLLM_ATTN"
|
assert attn.ring_runner.attn_backend_pref == "TRTLLM_ATTN"
|
||||||
|
assert attn.ring_runner.attn_backend_explicit is True
|
||||||
|
|
||||||
|
def test_attention_init_forwards_flashinfer_backend_without_dtype(self, monkeypatch):
|
||||||
|
class _FakeAttentionImpl:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
class _FakeBackend:
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "FLASHINFER_ATTN"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls():
|
||||||
|
return _FakeAttentionImpl
|
||||||
|
|
||||||
|
spec = AttentionSpec(backend="FLASHINFER_ATTN", quant={"flashinfer_backend": "fa2"})
|
||||||
|
|
||||||
|
monkeypatch.setattr(layer_mod, "get_attn_backend_for_role", lambda **kwargs: (_FakeBackend, spec))
|
||||||
|
monkeypatch.setattr(layer_mod.SDPABackend, "get_impl_cls", staticmethod(lambda: _FakeAttentionImpl))
|
||||||
|
monkeypatch.setattr(layer_mod, "build_parallel_attention_strategy", lambda **kwargs: object())
|
||||||
|
monkeypatch.setattr(layer_mod, "get_sp_group", lambda: None)
|
||||||
|
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: False)
|
||||||
|
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(default=spec),
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1),
|
||||||
|
diffusion_kv_cache_dtype=None,
|
||||||
|
diffusion_kv_cache_skip_step_indices=None,
|
||||||
|
diffusion_kv_cache_skip_layer_indices=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
attn = Attention(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
causal=False,
|
||||||
|
softmax_scale=1.0,
|
||||||
|
role="self",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert attn.backend_pref == "FLASHINFER_ATTN"
|
||||||
|
assert attn.attention.kwargs["backend_kwargs"] == {"quant": {"flashinfer_backend": "fa2"}}
|
||||||
|
assert attn.attention.kwargs["backend_explicit"] is True
|
||||||
|
|
||||||
|
def test_attention_init_propagates_ring_setup_failure(self, monkeypatch):
|
||||||
|
class _FakeAttentionImpl:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _FakeBackend:
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "TORCH_SDPA"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls():
|
||||||
|
return _FakeAttentionImpl
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
layer_mod,
|
||||||
|
"get_attn_backend_for_role",
|
||||||
|
lambda **kwargs: (_FakeBackend, AttentionSpec(backend="TORCH_SDPA")),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(layer_mod.SDPABackend, "get_impl_cls", staticmethod(lambda: _FakeAttentionImpl))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
layer_mod,
|
||||||
|
"get_sp_group",
|
||||||
|
lambda: (_ for _ in ()).throw(RuntimeError("SP group is not initialized")),
|
||||||
|
)
|
||||||
|
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(default="TORCH_SDPA"),
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=2),
|
||||||
|
)
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
with pytest.raises(RuntimeError, match="SP group is not initialized"):
|
||||||
|
Attention(num_heads=4, head_size=64, causal=False, softmax_scale=1.0)
|
||||||
|
|
||||||
|
def test_float32_main_dispatch_does_not_override_selected_backend(self):
|
||||||
|
sentinel = torch.randn(1, 2, 4, 8)
|
||||||
|
selected_calls = []
|
||||||
|
|
||||||
|
def _selected_forward(*args):
|
||||||
|
selected_calls.append(args)
|
||||||
|
return sentinel
|
||||||
|
|
||||||
|
fake_attention = SimpleNamespace(
|
||||||
|
attention=SimpleNamespace(
|
||||||
|
forward=_selected_forward,
|
||||||
|
),
|
||||||
|
sdpa_fallback=SimpleNamespace(
|
||||||
|
forward=lambda *args: pytest.fail("unexpected SDPA fallback"),
|
||||||
|
),
|
||||||
|
_assert_metadata_compatible=lambda metadata: None,
|
||||||
|
_scheduler_paged_kv=False,
|
||||||
|
paged_kv_cache_role=None,
|
||||||
|
)
|
||||||
|
query = torch.randn(1, 2, 4, 8, dtype=torch.float32)
|
||||||
|
|
||||||
|
output = Attention._run_local_attention(fake_attention, query, query, query, None)
|
||||||
|
|
||||||
|
assert output is sentinel
|
||||||
|
assert len(selected_calls) == 1
|
||||||
|
|
||||||
|
def test_unsupported_attention_mask_is_rejected_before_backend_call(self):
|
||||||
|
fake_backend = SimpleNamespace(
|
||||||
|
get_name=lambda: "NO_MASK_BACKEND",
|
||||||
|
supports_attention_mask=lambda *args, **kwargs: False,
|
||||||
|
)
|
||||||
|
fake_attention = SimpleNamespace(attn_backend=fake_backend)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.ones(1, 2, dtype=torch.bool))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not support attn_mask"):
|
||||||
|
Attention._assert_metadata_compatible(fake_attention, metadata)
|
||||||
|
|
||||||
|
def test_ring_attention_rejects_mask_instead_of_ignoring_it(self):
|
||||||
|
fake_attention = SimpleNamespace(attention=SimpleNamespace(skip=None), ring_runner=None)
|
||||||
|
query = torch.randn(1, 2, 4, 8)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.ones(1, 2, dtype=torch.bool))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Ring attention does not support attn_mask"):
|
||||||
|
Attention._run_ring_attention(fake_attention, query, query, query, metadata)
|
||||||
|
|
||||||
|
|
||||||
class TestDiffusionKvCacheQuantization:
|
class TestDiffusionKvCacheQuantization:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _install_attention_init_stubs(monkeypatch):
|
def _install_attention_init_stubs(monkeypatch, *, supports_kv_cache_dtype=True):
|
||||||
class _FakeAttentionImpl:
|
class _FakeAttentionImpl:
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
@@ -674,6 +947,10 @@ class TestDiffusionKvCacheQuantization:
|
|||||||
def forward(self, query, key, value, attn_metadata=None):
|
def forward(self, query, key, value, attn_metadata=None):
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def supports_kv_cache_dtype(kv_cache_dtype, platform_key) -> bool:
|
||||||
|
return supports_kv_cache_dtype
|
||||||
|
|
||||||
class _FakeBackend:
|
class _FakeBackend:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_name() -> str:
|
def get_name() -> str:
|
||||||
@@ -683,14 +960,11 @@ class TestDiffusionKvCacheQuantization:
|
|||||||
def get_impl_cls():
|
def get_impl_cls():
|
||||||
return _FakeAttentionImpl
|
return _FakeAttentionImpl
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def supports_kv_cache_dtype(kv_cache_dtype, platform_key) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
class _FakeRingParallelAttention:
|
class _FakeRingParallelAttention:
|
||||||
def __init__(self, sp_group, attn_backend_pref=None):
|
def __init__(self, sp_group, attn_backend_pref=None, attn_backend_explicit=False):
|
||||||
self.sp_group = sp_group
|
self.sp_group = sp_group
|
||||||
self.attn_backend_pref = attn_backend_pref
|
self.attn_backend_pref = attn_backend_pref
|
||||||
|
self.attn_backend_explicit = attn_backend_explicit
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
layer_mod,
|
layer_mod,
|
||||||
@@ -764,3 +1038,22 @@ class TestDiffusionKvCacheQuantization:
|
|||||||
causal=False,
|
causal=False,
|
||||||
softmax_scale=1.0,
|
softmax_scale=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_explicit_unsupported_kv_cache_dtype_raises(self, monkeypatch):
|
||||||
|
self._install_attention_init_stubs(monkeypatch, supports_kv_cache_dtype=False)
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(),
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1),
|
||||||
|
diffusion_kv_cache_dtype="fp8",
|
||||||
|
diffusion_kv_cache_skip_step_indices=None,
|
||||||
|
diffusion_kv_cache_skip_layer_indices=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
with pytest.raises(ValueError, match="does not support kv_cache_dtype='fp8'"):
|
||||||
|
Attention(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
causal=False,
|
||||||
|
softmax_scale=1.0,
|
||||||
|
)
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
from vllm.platforms.interface import DeviceCapability
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.attention.backends.registry import DiffusionAttentionBackendEnum
|
||||||
|
from vllm_omni.diffusion.attention.layer import Attention
|
||||||
|
from vllm_omni.diffusion.attention.selector import _cached_get_backend_cls
|
||||||
|
from vllm_omni.diffusion.config import set_current_diffusion_config
|
||||||
|
from vllm_omni.diffusion.data import AttentionConfig
|
||||||
|
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
|
||||||
|
from vllm_omni.diffusion.envs import PACKAGES_CHECKER
|
||||||
|
|
||||||
|
# Importing CudaOmniPlatform pulls in vllm.platforms.cuda, which imports the
|
||||||
|
# CUDA-only ``vllm._C_stable_libtorch`` extension at module top level. That
|
||||||
|
# extension is absent on non-CUDA builds (e.g. Intel/XPU), so skip the whole
|
||||||
|
# module there before the import can crash collection. ``is_cuda()`` resolves
|
||||||
|
# the platform without importing cuda.py.
|
||||||
|
if not current_platform.is_cuda():
|
||||||
|
pytest.skip("CUDA-only diffusion backend selection tests", allow_module_level=True)
|
||||||
|
|
||||||
|
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform # noqa: E402
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
def _blackwell_sm120(monkeypatch: pytest.MonkeyPatch, *, cudnn_version: int = 90500) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(12, 0)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": False})
|
||||||
|
monkeypatch.setattr(CudaOmniPlatform, "has_flash_attn_4", classmethod(lambda cls: False))
|
||||||
|
monkeypatch.setattr(torch.backends.cudnn, "version", lambda: cudnn_version)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_dummy_flashinfer(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Top-level ``flashinfer`` only — no ``prefill`` wrapper (partial install)."""
|
||||||
|
monkeypatch.setitem(sys.modules, "flashinfer", types.ModuleType("flashinfer"))
|
||||||
|
# A previously imported real submodule must not satisfy the wrapper probe.
|
||||||
|
monkeypatch.delitem(sys.modules, "flashinfer.prefill", raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_dummy_flashinfer_prefill(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""FlashInfer stub that exposes the prefill wrapper FLASHINFER_ATTN needs."""
|
||||||
|
prefill = types.ModuleType("flashinfer.prefill")
|
||||||
|
setattr(prefill, "BatchPrefillWithRaggedKVCacheWrapper", type("BatchPrefillWithRaggedKVCacheWrapper", (), {}))
|
||||||
|
pkg = types.ModuleType("flashinfer")
|
||||||
|
setattr(pkg, "prefill", prefill)
|
||||||
|
monkeypatch.setitem(sys.modules, "flashinfer", pkg)
|
||||||
|
monkeypatch.setitem(sys.modules, "flashinfer.prefill", prefill)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_selects_cudnn_for_supported_blackwell_head_size(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=128)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.CUDNN_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_routes_incompatible_head_size_to_sdpa(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
_install_dummy_flashinfer(monkeypatch)
|
||||||
|
|
||||||
|
# 320 is outside cuDNN FMHA (<=256, multiple of 8) and FlashInfer {64,128,256}.
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=320)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_routes_non_multiple_head_size_to_sdpa_without_flashinfer(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
monkeypatch.setitem(sys.modules, "flashinfer", None)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=12)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_selects_flashinfer_when_cudnn_too_old(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch, cudnn_version=90499)
|
||||||
|
_install_dummy_flashinfer_prefill(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=128)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.FLASHINFER_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_skips_flashinfer_when_only_toplevel_package(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
"""A stub that only imports ``flashinfer`` must not be auto-selected."""
|
||||||
|
_blackwell_sm120(monkeypatch, cudnn_version=90499)
|
||||||
|
_install_dummy_flashinfer(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=128)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_flashinfer_raises_without_prefill_wrapper(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
_install_dummy_flashinfer(monkeypatch)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="BatchPrefillWithRaggedKVCacheWrapper"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASHINFER_ATTN", head_size=128)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_skips_flashinfer_for_unsupported_head_size_when_cudnn_too_old(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch, cudnn_version=90499)
|
||||||
|
_install_dummy_flashinfer(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=72)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_cudnn_raises_for_incompatible_head_size(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="head_size=12 is unsupported"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("CUDNN_ATTN", head_size=12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_cudnn_accepts_supported_head_size(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls("CUDNN_ATTN", head_size=72)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.CUDNN_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_cudnn_skips_head_size_check_for_capability_probe(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls("CUDNN_ATTN", head_size=-1)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.CUDNN_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("head_size", [32, 320])
|
||||||
|
def test_explicit_flashinfer_raises_for_unsupported_head_size(monkeypatch: pytest.MonkeyPatch, head_size: int):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
_install_dummy_flashinfer_prefill(monkeypatch)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=f"head_size={head_size} is unsupported"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASHINFER_ATTN", head_size=head_size)
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_flashinfer_accepts_supported_head_size(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
_install_dummy_flashinfer_prefill(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASHINFER_ATTN", head_size=128)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.FLASHINFER_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_flashinfer_skips_head_size_check_for_capability_probe(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
_install_dummy_flashinfer_prefill(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASHINFER_ATTN", head_size=-1)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.FLASHINFER_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_selects_cudnn_for_unknown_head_size_probe(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
_install_dummy_flashinfer_prefill(monkeypatch)
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=-1)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.CUDNN_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_flash_attn_rejects_blackwell_without_fa4(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": True})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="requires CuTe FlashAttention-4"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN", head_size=64)
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_flash_attn_accepts_blackwell_when_fa4_available(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
monkeypatch.setattr(CudaOmniPlatform, "has_flash_attn_4", classmethod(lambda cls: True))
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN", head_size=64)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.FLASH_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_blackwell_dense_flash_capability_tracks_fa4(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
|
||||||
|
assert CudaOmniPlatform.supports_diffusion_dense_flash_attention() is False
|
||||||
|
|
||||||
|
monkeypatch.setattr(CudaOmniPlatform, "has_flash_attn_4", classmethod(lambda cls: True))
|
||||||
|
|
||||||
|
assert CudaOmniPlatform.supports_diffusion_dense_flash_attention() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_hopper_dense_flash_capability_tracks_fa_package(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(9, 0)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(CudaOmniPlatform, "has_flash_attn_package", classmethod(lambda cls: False))
|
||||||
|
|
||||||
|
assert CudaOmniPlatform.supports_diffusion_dense_flash_attention() is False
|
||||||
|
|
||||||
|
monkeypatch.setattr(CudaOmniPlatform, "has_flash_attn_package", classmethod(lambda cls: True))
|
||||||
|
|
||||||
|
assert CudaOmniPlatform.supports_diffusion_dense_flash_attention() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_selects_flash_attn_on_blackwell_when_cudnn_old_and_fa4_available(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
):
|
||||||
|
_blackwell_sm120(monkeypatch, cudnn_version=90499)
|
||||||
|
monkeypatch.setitem(sys.modules, "flashinfer", None)
|
||||||
|
monkeypatch.setattr(CudaOmniPlatform, "has_flash_attn_4", classmethod(lambda cls: True))
|
||||||
|
|
||||||
|
path = CudaOmniPlatform.get_diffusion_attn_backend_cls(None, head_size=128)
|
||||||
|
|
||||||
|
assert path == DiffusionAttentionBackendEnum.FLASH_ATTN.get_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_marked_paged_layer_constructs_on_blackwell_cudnn_without_fa4(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
"""Scheduler-paged remap must not require dense FA4 when CUDNN is the default."""
|
||||||
|
_blackwell_sm120(monkeypatch)
|
||||||
|
_cached_get_backend_cls.cache_clear()
|
||||||
|
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(),
|
||||||
|
diffusion_kv_mode=DiffusionKVCacheMode.PAGED_SCHEDULER,
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1, allgather_degree=1),
|
||||||
|
diffusion_kv_cache_dtype=None,
|
||||||
|
diffusion_kv_cache_skip_step_indices=None,
|
||||||
|
diffusion_kv_cache_skip_layer_indices=None,
|
||||||
|
model_class_name=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
attention = Attention(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
causal=False,
|
||||||
|
softmax_scale=1.0,
|
||||||
|
paged_kv_cache_role="primary",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert attention.attn_backend.get_name() == "FLASH_ATTN"
|
||||||
|
assert attention.backend_pref == "FLASH_ATTN"
|
||||||
|
assert attention.backend_explicit is False
|
||||||
|
assert attention.attn_spec is None
|
||||||
@@ -1,62 +1,84 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
from contextlib import nullcontext
|
from contextlib import contextmanager, nullcontext
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
from torch.nn.attention import SDPBackend
|
from torch.nn.attention import SDPBackend
|
||||||
|
|
||||||
|
import vllm_omni.diffusion.attention.backends.cudnn_attn as cudnn_backend
|
||||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||||
from vllm_omni.diffusion.attention.backends.cudnn_attn import CuDNNAttentionImpl
|
from vllm_omni.diffusion.attention.backends.cudnn_attn import CuDNNAttentionBackend, CuDNNAttentionImpl
|
||||||
|
|
||||||
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
def test_cudnn_backend_uses_math_for_kv_seq_len_one(monkeypatch):
|
||||||
("query_sequence_length", "key_sequence_length", "expected_backend"),
|
"""Automatic CUDNN_ATTN (platform default) may use MATH for singleton K/V."""
|
||||||
[
|
|
||||||
(1, 1, SDPBackend.MATH),
|
|
||||||
(8, 1, SDPBackend.MATH),
|
|
||||||
(1, 8, SDPBackend.MATH),
|
|
||||||
(8, 8, SDPBackend.CUDNN_ATTENTION),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_cudnn_routes_single_token_attention_to_math(
|
|
||||||
monkeypatch, query_sequence_length, key_sequence_length, expected_backend
|
|
||||||
):
|
|
||||||
selected_backends = []
|
selected_backends = []
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
def fake_sdpa_kernel(backends):
|
def fake_sdpa_kernel(backends):
|
||||||
selected_backends.append(backends)
|
selected_backends.append(tuple(backends))
|
||||||
return nullcontext()
|
yield
|
||||||
|
|
||||||
monkeypatch.setattr(
|
def fake_sdpa(query, key, value, **kwargs):
|
||||||
"vllm_omni.diffusion.attention.backends.cudnn_attn.sdpa_kernel",
|
return query
|
||||||
fake_sdpa_kernel,
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
torch.nn.functional,
|
|
||||||
"scaled_dot_product_attention",
|
|
||||||
lambda query, _key, _value, **_kwargs: query,
|
|
||||||
)
|
|
||||||
impl = CuDNNAttentionImpl(
|
|
||||||
num_heads=2,
|
|
||||||
head_size=4,
|
|
||||||
softmax_scale=0.5,
|
|
||||||
)
|
|
||||||
query = torch.randn(1, query_sequence_length, 2, 4)
|
|
||||||
key = torch.randn(1, key_sequence_length, 2, 4)
|
|
||||||
value = torch.randn_like(key)
|
|
||||||
|
|
||||||
output = impl.forward_cuda(query, key, value)
|
monkeypatch.setattr(cudnn_backend, "sdpa_kernel", fake_sdpa_kernel)
|
||||||
|
monkeypatch.setattr(torch.nn.functional, "scaled_dot_product_attention", fake_sdpa)
|
||||||
|
|
||||||
|
impl = CuDNNAttentionImpl(num_heads=2, head_size=8, softmax_scale=0.5)
|
||||||
|
query = torch.randn(1, 2, 2, 8)
|
||||||
|
singleton_kv = torch.randn(1, 1, 2, 8)
|
||||||
|
|
||||||
|
output = impl.forward_cuda(query, singleton_kv, singleton_kv)
|
||||||
|
|
||||||
assert output.shape == query.shape
|
assert output.shape == query.shape
|
||||||
assert selected_backends == [[expected_backend]]
|
assert selected_backends == [(SDPBackend.MATH,)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_cudnn_rejects_kv_seq_len_one():
|
||||||
|
impl = CuDNNAttentionImpl(
|
||||||
|
num_heads=2,
|
||||||
|
head_size=8,
|
||||||
|
softmax_scale=0.5,
|
||||||
|
backend_explicit=True,
|
||||||
|
)
|
||||||
|
query = torch.randn(1, 2, 2, 8)
|
||||||
|
singleton_kv = torch.randn(1, 1, 2, 8)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="explicitly selected.*sequence length 1"):
|
||||||
|
impl.forward_cuda(query, singleton_kv, singleton_kv)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cudnn_backend_pins_cudnn_only_when_kv_seq_len_gt_one(monkeypatch):
|
||||||
|
selected_backends = []
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def fake_sdpa_kernel(backends):
|
||||||
|
selected_backends.append(tuple(backends))
|
||||||
|
yield
|
||||||
|
|
||||||
|
def reject_shape(*args, **kwargs):
|
||||||
|
raise RuntimeError("No available kernel. Aborting execution.")
|
||||||
|
|
||||||
|
monkeypatch.setattr(cudnn_backend, "sdpa_kernel", fake_sdpa_kernel)
|
||||||
|
monkeypatch.setattr(torch.nn.functional, "scaled_dot_product_attention", reject_shape)
|
||||||
|
|
||||||
|
impl = CuDNNAttentionImpl(num_heads=2, head_size=8, softmax_scale=0.5)
|
||||||
|
tensors = torch.randn(1, 2, 2, 8)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="No available kernel"):
|
||||||
|
impl.forward_cuda(tensors, tensors, tensors)
|
||||||
|
|
||||||
|
assert selected_backends == [(SDPBackend.CUDNN_ATTENTION,)]
|
||||||
|
|
||||||
|
|
||||||
def test_cudnn_slices_valid_kv_prefix_without_padding_mask(monkeypatch):
|
def test_cudnn_slices_valid_kv_prefix_without_padding_mask(monkeypatch):
|
||||||
observed = {}
|
observed: dict[str, Any] = {}
|
||||||
|
|
||||||
def fake_sdpa(query, key, value, **kwargs):
|
def fake_sdpa(query, key, value, **kwargs):
|
||||||
observed.update(query=query, key=key, value=value, kwargs=kwargs)
|
observed.update(query=query, key=key, value=value, kwargs=kwargs)
|
||||||
@@ -105,3 +127,15 @@ def test_cudnn_rejects_invalid_valid_kv_length():
|
|||||||
query,
|
query,
|
||||||
AttentionMetadata(extra={"valid_kv_length": 9}),
|
AttentionMetadata(extra={"valid_kv_length": 9}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("head_size", [8, 64, 128, 256])
|
||||||
|
def test_cudnn_backend_accepts_blackwell_fmha_head_sizes(head_size):
|
||||||
|
assert CuDNNAttentionBackend.supports_head_size(head_size)
|
||||||
|
assert head_size in CuDNNAttentionBackend.get_supported_head_sizes()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("head_size", [0, 7, 12, 320])
|
||||||
|
def test_cudnn_backend_rejects_incompatible_head_sizes(head_size):
|
||||||
|
assert not CuDNNAttentionBackend.supports_head_size(head_size)
|
||||||
|
assert head_size not in CuDNNAttentionBackend.get_supported_head_sizes()
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.attention.backends import flashinfer_attn
|
||||||
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||||
|
from vllm_omni.diffusion.attention.backends.flashinfer_attn import (
|
||||||
|
FlashInferAttentionBackend,
|
||||||
|
FlashInferAttentionImpl,
|
||||||
|
)
|
||||||
|
from vllm_omni.diffusion.data import AttentionSpec, AttnQuantSpec
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
def _impl(*, causal: bool = False, backend_explicit: bool = False):
|
||||||
|
# Avoid CUDA/wrapper init; these tests only cover mask validation.
|
||||||
|
obj = FlashInferAttentionImpl.__new__(FlashInferAttentionImpl)
|
||||||
|
obj.causal = causal
|
||||||
|
obj.softmax_scale = 0.5
|
||||||
|
obj.flashinfer_backend = "fa2"
|
||||||
|
obj.backend_explicit = backend_explicit
|
||||||
|
obj._sdpa_fallback = None
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def test_flashinfer_rejects_float_mask_instead_of_falling_back(monkeypatch):
|
||||||
|
monkeypatch.setattr(flashinfer_attn, "HAS_FLASHINFER", True)
|
||||||
|
query = torch.randn(1, 2, 2, 8)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.zeros(2, 2))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="boolean-only"):
|
||||||
|
_impl(backend_explicit=True).forward_cuda(query, query, query, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flashinfer_rejects_causal_custom_mask_instead_of_falling_back(monkeypatch):
|
||||||
|
monkeypatch.setattr(flashinfer_attn, "HAS_FLASHINFER", True)
|
||||||
|
query = torch.randn(1, 2, 2, 8)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.tensor([[True, False], [True, True]]))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="causal=True"):
|
||||||
|
_impl(causal=True, backend_explicit=True).forward_cuda(query, query, query, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_cute_dsl_rejects_custom_mask_instead_of_falling_back(monkeypatch):
|
||||||
|
monkeypatch.setattr(flashinfer_attn, "HAS_FLASHINFER", True)
|
||||||
|
impl = _impl(backend_explicit=True)
|
||||||
|
impl.flashinfer_backend = "cute-dsl"
|
||||||
|
query = torch.randn(1, 2, 2, 8)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.tensor([[True, False], [True, True]]))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="cute-dsl"):
|
||||||
|
impl.forward_cuda(query, query, query, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_cute_dsl_falls_back_to_sdpa_for_custom_mask(monkeypatch):
|
||||||
|
monkeypatch.setattr(flashinfer_attn, "HAS_FLASHINFER", True)
|
||||||
|
impl = _impl(backend_explicit=False)
|
||||||
|
impl.flashinfer_backend = "cute-dsl"
|
||||||
|
impl._run_batch_prefill = lambda *args, **kwargs: pytest.fail("unexpected cute-dsl prefill")
|
||||||
|
query = torch.randn(1, 2, 2, 8)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.tensor([[True, False], [True, True]]))
|
||||||
|
|
||||||
|
output = impl.forward_cuda(query, query, query, metadata)
|
||||||
|
|
||||||
|
assert output.shape == query.shape
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_cute_dsl_is_not_mask_capable(monkeypatch):
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *args, **kwargs: (10, 0))
|
||||||
|
spec = AttentionSpec(backend="FLASHINFER_ATTN")
|
||||||
|
|
||||||
|
assert FlashInferAttentionBackend.supports_attention_mask() is True
|
||||||
|
assert FlashInferAttentionBackend.supports_attention_mask(spec) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_fa2_flashinfer_is_mask_capable(monkeypatch):
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *args, **kwargs: (10, 0))
|
||||||
|
spec = AttentionSpec(backend="FLASHINFER_ATTN", quant=AttnQuantSpec(flashinfer_backend="fa2"))
|
||||||
|
|
||||||
|
assert FlashInferAttentionBackend.supports_attention_mask(spec) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_fa2_backend_kwargs_select_fa2_on_blackwell(monkeypatch):
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *args, **kwargs: (10, 0))
|
||||||
|
spec = AttentionSpec(backend="FLASHINFER_ATTN", quant=AttnQuantSpec(flashinfer_backend="fa2"))
|
||||||
|
kwargs = spec.backend_kwargs() or {}
|
||||||
|
requested = (kwargs.get("quant") or {}).get("flashinfer_backend", "auto")
|
||||||
|
|
||||||
|
assert kwargs == {"quant": {"flashinfer_backend": "fa2"}}
|
||||||
|
assert FlashInferAttentionImpl._select_backend(requested) == "fa2"
|
||||||
|
assert FlashInferAttentionBackend.supports_attention_mask(spec) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_flashinfer_backend_kwargs_select_cute_dsl_on_blackwell(monkeypatch):
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *args, **kwargs: (10, 0))
|
||||||
|
spec = AttentionSpec(backend="FLASHINFER_ATTN")
|
||||||
|
kwargs = spec.backend_kwargs() or {}
|
||||||
|
requested = (kwargs.get("quant") or {}).get("flashinfer_backend", "auto")
|
||||||
|
|
||||||
|
assert "quant" not in kwargs
|
||||||
|
assert FlashInferAttentionImpl._select_backend(requested) == "cute-dsl"
|
||||||
|
assert FlashInferAttentionBackend.supports_attention_mask(spec) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_hopper_explicit_flashinfer_is_mask_capable(monkeypatch):
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *args, **kwargs: (9, 0))
|
||||||
|
spec = AttentionSpec(backend="FLASHINFER_ATTN")
|
||||||
|
|
||||||
|
assert FlashInferAttentionBackend.supports_attention_mask(spec) is True
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
@@ -9,18 +9,13 @@ import torch
|
|||||||
|
|
||||||
from tests.helpers.mark import hardware_test
|
from tests.helpers.mark import hardware_test
|
||||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||||
from vllm_omni.diffusion.attention.backends.registry import DiffusionAttentionBackendEnum
|
|
||||||
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
|
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core_model
|
@pytest.mark.core_model
|
||||||
@pytest.mark.cpu
|
@pytest.mark.cpu
|
||||||
def test_kernels_hub_platform_fallback(monkeypatch: pytest.MonkeyPatch):
|
def test_explicit_kernels_hub_selection_does_not_fallback(monkeypatch: pytest.MonkeyPatch):
|
||||||
"""Test that when kernels package is not available, the platform fallback logic
|
|
||||||
|
|
||||||
routes to local FLASH_ATTN.
|
|
||||||
"""
|
|
||||||
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
||||||
|
|
||||||
# Temporarily hide/remove kernels module if present
|
# Temporarily hide/remove kernels module if present
|
||||||
@@ -38,19 +33,73 @@ def test_kernels_hub_platform_fallback(monkeypatch: pytest.MonkeyPatch):
|
|||||||
)
|
)
|
||||||
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": True})
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": True})
|
||||||
|
|
||||||
# Test FLASH_ATTN_HUB falls back to FLASH_ATTN when kernels is unavailable
|
with pytest.raises(ImportError, match="explicitly selected"):
|
||||||
backend_path = CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_HUB", head_size=64)
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_HUB", head_size=64)
|
||||||
assert backend_path == DiffusionAttentionBackendEnum.FLASH_ATTN.get_path()
|
with pytest.raises(ImportError, match="explicitly selected"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_3_HUB", head_size=64)
|
||||||
|
|
||||||
# Test FLASH_ATTN_3_HUB falls back to FLASH_ATTN when kernels is unavailable
|
|
||||||
backend_path = CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_3_HUB", head_size=64)
|
|
||||||
assert backend_path == DiffusionAttentionBackendEnum.FLASH_ATTN.get_path()
|
|
||||||
|
|
||||||
# Test FLASH_ATTN_3_HUB falls back to FLASH_ATTN_HUB on pre-Hopper GPUs
|
|
||||||
kernels_module = types.ModuleType("kernels")
|
kernels_module = types.ModuleType("kernels")
|
||||||
monkeypatch.setitem(sys.modules, "kernels", kernels_module)
|
monkeypatch.setitem(sys.modules, "kernels", kernels_module)
|
||||||
backend_path = CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_3_HUB", head_size=64)
|
with pytest.raises(ValueError, match="require.*Hopper GPU"):
|
||||||
assert backend_path == DiffusionAttentionBackendEnum.FLASH_ATTN_HUB.get_path()
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_3_HUB", head_size=64)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(10, 3)),
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="current FA2 kernels require"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_HUB", head_size=64)
|
||||||
|
with pytest.raises(ValueError, match="require.*Hopper GPU"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN_3_HUB", head_size=64)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core_model
|
||||||
|
@pytest.mark.cpu
|
||||||
|
def test_explicit_flash_attention_unavailable_does_not_fallback(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
from vllm.platforms.interface import DeviceCapability
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.envs import PACKAGES_CHECKER
|
||||||
|
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(8, 0)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": False})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="explicitly selected but is unsupported"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN", head_size=64)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(10, 3)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": True})
|
||||||
|
monkeypatch.setattr(CudaOmniPlatform, "has_flash_attn_4", classmethod(lambda cls: False))
|
||||||
|
with pytest.raises(ValueError, match="requires CuTe FlashAttention-4"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("FLASH_ATTN", head_size=64)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core_model
|
||||||
|
@pytest.mark.cpu
|
||||||
|
def test_explicit_sage_attention_unsupported_arch_does_not_reach_kernel(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
from vllm.platforms.interface import DeviceCapability
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.envs import PACKAGES_CHECKER
|
||||||
|
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(10, 3)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": False})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not provide a kernel for sm_103"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("SAGE_ATTN", head_size=64)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core_model
|
@pytest.mark.core_model
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.attention.backends.ring import ring_globals, ring_kernels
|
||||||
|
from vllm_omni.diffusion.attention.parallel import factory, ring
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_build_fa3_is_hopper_only(monkeypatch):
|
||||||
|
fake_interface = SimpleNamespace(
|
||||||
|
_flash_attn_forward=lambda *args, **kwargs: None,
|
||||||
|
flash_attn_func=lambda *args, **kwargs: None,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with monkeypatch.context() as context:
|
||||||
|
context.setitem(sys.modules, "flash_attn_interface", fake_interface)
|
||||||
|
reloaded = importlib.reload(ring_globals)
|
||||||
|
assert reloaded.HAS_FA3
|
||||||
|
assert reloaded.FA3_SUPPORTED_CUDA_MAJORS == frozenset({9})
|
||||||
|
finally:
|
||||||
|
importlib.reload(ring_globals)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("capability", "expected"),
|
||||||
|
[
|
||||||
|
((8, 0), True),
|
||||||
|
((8, 9), True),
|
||||||
|
((9, 0), True),
|
||||||
|
((10, 0), False),
|
||||||
|
((10, 3), False),
|
||||||
|
((12, 0), False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_can_use_fa3_checks_device_arch(monkeypatch, capability, expected):
|
||||||
|
monkeypatch.setattr(ring, "HAS_FA3", True)
|
||||||
|
monkeypatch.setattr(ring, "FA3_SUPPORTED_CUDA_MAJORS", frozenset({8, 9}))
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda _device: capability)
|
||||||
|
|
||||||
|
assert ring._can_use_fa3(torch.device("cuda")) is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_use_fa3_preserves_unknown_extension_contract(monkeypatch):
|
||||||
|
monkeypatch.setattr(ring, "HAS_FA3", True)
|
||||||
|
monkeypatch.setattr(ring, "FA3_SUPPORTED_CUDA_MAJORS", None)
|
||||||
|
|
||||||
|
assert ring._can_use_fa3(torch.device("cuda"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("capability", "expected"),
|
||||||
|
[
|
||||||
|
((8, 0), True),
|
||||||
|
((9, 0), True),
|
||||||
|
((10, 0), False),
|
||||||
|
((10, 3), False),
|
||||||
|
((12, 0), False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_can_use_fa2_checks_device_arch(monkeypatch, capability, expected):
|
||||||
|
monkeypatch.setattr(ring, "HAS_FLASH_ATTN", True)
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda _device: capability)
|
||||||
|
|
||||||
|
assert ring._can_use_fa2(torch.device("cuda")) is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_ring_falls_back_to_sdpa_when_fa3_extension_has_no_device_kernel(monkeypatch):
|
||||||
|
monkeypatch.setattr(ring, "HAS_FA3", True)
|
||||||
|
monkeypatch.setattr(ring, "HAS_FA4", False)
|
||||||
|
monkeypatch.setattr(ring, "HAS_FLASH_ATTN", False)
|
||||||
|
monkeypatch.setattr(ring, "HAS_AITER", False)
|
||||||
|
monkeypatch.setattr(ring, "_can_use_fa3", lambda _device: False)
|
||||||
|
|
||||||
|
expected = torch.randn(1, 2, 1, 8)
|
||||||
|
|
||||||
|
def fake_ring_sdpa(*args, **kwargs):
|
||||||
|
return expected
|
||||||
|
|
||||||
|
module_name = "vllm_omni.diffusion.attention.backends.ring_pytorch_attn"
|
||||||
|
monkeypatch.setitem(sys.modules, module_name, SimpleNamespace(ring_pytorch_attn_func=fake_ring_sdpa))
|
||||||
|
|
||||||
|
strategy = ring.RingParallelAttention(SimpleNamespace(ring_group=object()))
|
||||||
|
query = torch.randn(1, 2, 1, 8, dtype=torch.bfloat16)
|
||||||
|
actual = strategy.run_attention(query, query, query, attn_metadata=None)
|
||||||
|
|
||||||
|
assert actual is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_ring_backend_does_not_fallback_to_sdpa(monkeypatch):
|
||||||
|
monkeypatch.setattr(ring, "HAS_FA3", True)
|
||||||
|
monkeypatch.setattr(ring, "HAS_FA4", False)
|
||||||
|
monkeypatch.setattr(ring, "HAS_FLASH_ATTN", False)
|
||||||
|
monkeypatch.setattr(ring, "HAS_AITER", False)
|
||||||
|
monkeypatch.setattr(ring, "_can_use_fa3", lambda _device: False)
|
||||||
|
|
||||||
|
strategy = ring.RingParallelAttention(
|
||||||
|
SimpleNamespace(ring_group=object()),
|
||||||
|
attn_backend_pref="FLASH_ATTN",
|
||||||
|
attn_backend_explicit=True,
|
||||||
|
)
|
||||||
|
query = torch.randn(1, 2, 1, 8, dtype=torch.bfloat16)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="explicitly selected"):
|
||||||
|
strategy.run_attention(query, query, query, attn_metadata=None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"backend_pref",
|
||||||
|
[
|
||||||
|
"CUDNN_ATTN",
|
||||||
|
"FLASH_ATTN_HUB",
|
||||||
|
"FLASH_ATTN_3_HUB",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_explicit_non_ring_backend_is_rejected(monkeypatch, backend_pref):
|
||||||
|
# Local FA is available so a missing rejection would silently run FA4/FA3/FA2
|
||||||
|
# instead of the requested Hub/cuDNN backend.
|
||||||
|
monkeypatch.setattr(ring, "HAS_FA4", True)
|
||||||
|
monkeypatch.setattr(ring, "_can_use_fa4", lambda _device: True)
|
||||||
|
module_name = "vllm_omni.diffusion.attention.backends.ring_pytorch_attn"
|
||||||
|
monkeypatch.setitem(
|
||||||
|
sys.modules,
|
||||||
|
module_name,
|
||||||
|
SimpleNamespace(ring_pytorch_attn_func=lambda *args, **kwargs: pytest.fail("unexpected SDPA fallback")),
|
||||||
|
)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
sys.modules,
|
||||||
|
"vllm_omni.diffusion.attention.backends.ring_flash_attn",
|
||||||
|
SimpleNamespace(ring_flash_attn_func=lambda *args, **kwargs: pytest.fail("unexpected local FA ring")),
|
||||||
|
)
|
||||||
|
strategy = ring.RingParallelAttention(
|
||||||
|
SimpleNamespace(ring_group=object()),
|
||||||
|
attn_backend_pref=backend_pref,
|
||||||
|
attn_backend_explicit=True,
|
||||||
|
)
|
||||||
|
query = torch.randn(1, 2, 1, 8, dtype=torch.bfloat16)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="ring sequence parallelism has no implementation"):
|
||||||
|
strategy.run_attention(query, query, query, attn_metadata=None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_flash_ring_rejects_float32_instead_of_falling_back(monkeypatch):
|
||||||
|
module_name = "vllm_omni.diffusion.attention.backends.ring_pytorch_attn"
|
||||||
|
monkeypatch.setitem(
|
||||||
|
sys.modules,
|
||||||
|
module_name,
|
||||||
|
SimpleNamespace(ring_pytorch_attn_func=lambda *args, **kwargs: pytest.fail("unexpected SDPA fallback")),
|
||||||
|
)
|
||||||
|
strategy = ring.RingParallelAttention(
|
||||||
|
SimpleNamespace(ring_group=object()),
|
||||||
|
attn_backend_pref="FLASH_ATTN",
|
||||||
|
attn_backend_explicit=True,
|
||||||
|
)
|
||||||
|
query = torch.randn(1, 2, 1, 8, dtype=torch.float32)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not support float32"):
|
||||||
|
strategy.run_attention(query, query, query, attn_metadata=None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pytorch_ring_flash_op_rejects_float32():
|
||||||
|
query = torch.randn(1, 2, 1, 8, dtype=torch.float32)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not support float32"):
|
||||||
|
ring_kernels.pytorch_attn_forward(query, query, query, op_type="flash")
|
||||||
|
|
||||||
|
|
||||||
|
def test_configured_sp_does_not_fallback_when_group_is_unavailable(monkeypatch):
|
||||||
|
parallel_config = SimpleNamespace(ulysses_degree=2, ring_degree=1, allgather_degree=1)
|
||||||
|
forward_context = SimpleNamespace(omni_diffusion_config=SimpleNamespace(parallel_config=parallel_config))
|
||||||
|
monkeypatch.setattr(factory, "is_forward_context_available", lambda: True)
|
||||||
|
monkeypatch.setattr(factory, "get_forward_context", lambda: forward_context)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
factory,
|
||||||
|
"get_sp_group",
|
||||||
|
lambda: (_ for _ in ()).throw(RuntimeError("SP group is not initialized")),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="SP is configured"):
|
||||||
|
factory.build_parallel_attention_strategy(scatter_idx=2, gather_idx=1, use_sync=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_configured_sp_rejects_single_rank_group(monkeypatch):
|
||||||
|
parallel_config = SimpleNamespace(ulysses_degree=2, ring_degree=1, allgather_degree=1)
|
||||||
|
forward_context = SimpleNamespace(omni_diffusion_config=SimpleNamespace(parallel_config=parallel_config))
|
||||||
|
monkeypatch.setattr(factory, "is_forward_context_available", lambda: True)
|
||||||
|
monkeypatch.setattr(factory, "get_forward_context", lambda: forward_context)
|
||||||
|
monkeypatch.setattr(factory, "get_sp_group", lambda: SimpleNamespace())
|
||||||
|
monkeypatch.setattr(factory, "get_sequence_parallel_world_size", lambda: 1)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="world size is not greater than one"):
|
||||||
|
factory.build_parallel_attention_strategy(scatter_idx=2, gather_idx=1, use_sync=False)
|
||||||
@@ -72,3 +72,25 @@ def test_ring_prefers_fa4_on_blackwell(monkeypatch):
|
|||||||
|
|
||||||
assert strategy.run_attention(query, "key", "value", None) == "output"
|
assert strategy.run_attention(query, "key", "value", None) == "output"
|
||||||
assert captured["attn_type"] is AttnType.FA4
|
assert captured["attn_type"] is AttnType.FA4
|
||||||
|
|
||||||
|
|
||||||
|
def test_flashinfer_wrapper_canonicalizes_unbatched_lse_to_bhs(monkeypatch):
|
||||||
|
seq_len = num_heads = 4
|
||||||
|
query = torch.randn(1, seq_len, num_heads, 8)
|
||||||
|
unbatched_out = torch.randn(seq_len, num_heads, 8)
|
||||||
|
unbatched_lse = torch.arange(num_heads * seq_len, dtype=torch.float32).reshape(num_heads, seq_len)
|
||||||
|
assert not torch.equal(unbatched_lse, unbatched_lse.transpose(0, 1))
|
||||||
|
|
||||||
|
def fake_prefill(_q, _k, _v, **_kwargs):
|
||||||
|
return unbatched_out.clone(), unbatched_lse.clone()
|
||||||
|
|
||||||
|
monkeypatch.setattr(ring_kernels, "HAS_FLASHINFER", True)
|
||||||
|
# FlashInfer symbols are only bound when HAS_FLASHINFER was true at import.
|
||||||
|
monkeypatch.setattr(ring_kernels, "single_prefill_with_kv_cache", fake_prefill, raising=False)
|
||||||
|
monkeypatch.setattr(ring_kernels, "_LOG2_E", 1.0, raising=False)
|
||||||
|
|
||||||
|
out, lse = ring_kernels.flashinfer_attn_forward(query, query, query, softmax_scale=0.1)
|
||||||
|
|
||||||
|
assert out.shape == query.shape
|
||||||
|
assert lse.shape == (1, num_heads, seq_len)
|
||||||
|
assert torch.equal(lse[0], unbatched_lse)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.attention.backends.ring.ring_utils import update_out_and_lse
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("layout", ["bhs", "bsh", "bhs_padded", "bhs1"])
|
||||||
|
def test_ring_lse_known_layouts_are_normalized(layout):
|
||||||
|
block_out = torch.randn(2, 5, 3, 4)
|
||||||
|
canonical = torch.randn(2, 5, 3)
|
||||||
|
if layout == "bhs":
|
||||||
|
block_lse = canonical.transpose(1, 2)
|
||||||
|
lse_layout = "bhs"
|
||||||
|
elif layout == "bsh":
|
||||||
|
block_lse = canonical
|
||||||
|
lse_layout = "bsh"
|
||||||
|
elif layout == "bhs_padded":
|
||||||
|
block_lse = torch.cat([canonical.transpose(1, 2), torch.randn(2, 3, 2)], dim=2)
|
||||||
|
lse_layout = "bhs"
|
||||||
|
else:
|
||||||
|
block_lse = canonical.transpose(1, 2).unsqueeze(-1)
|
||||||
|
lse_layout = "bhs"
|
||||||
|
|
||||||
|
out, lse = update_out_and_lse(None, None, block_out, block_lse, lse_layout=lse_layout)
|
||||||
|
|
||||||
|
assert out.shape == block_out.shape
|
||||||
|
assert out.dtype == torch.float32
|
||||||
|
assert lse.shape == (2, 5, 3, 1)
|
||||||
|
assert torch.equal(lse.squeeze(-1), canonical)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ring_lse_seq_len_equals_num_heads_keeps_asymmetric_values():
|
||||||
|
batch, seq_len, num_heads, head_dim = 2, 4, 4, 8
|
||||||
|
block_out = torch.randn(batch, seq_len, num_heads, head_dim)
|
||||||
|
canonical = torch.arange(batch * seq_len * num_heads, dtype=torch.float32).reshape(batch, seq_len, num_heads)
|
||||||
|
bhs = canonical.transpose(1, 2)
|
||||||
|
|
||||||
|
assert bhs.shape == canonical.shape
|
||||||
|
assert not torch.equal(bhs, canonical)
|
||||||
|
|
||||||
|
_, lse_from_bhs = update_out_and_lse(None, None, block_out, bhs, lse_layout="bhs")
|
||||||
|
_, lse_from_bsh = update_out_and_lse(None, None, block_out, canonical, lse_layout="bsh")
|
||||||
|
|
||||||
|
assert torch.equal(lse_from_bhs.squeeze(-1), canonical)
|
||||||
|
assert torch.equal(lse_from_bsh.squeeze(-1), canonical)
|
||||||
|
|
||||||
|
_, lse_if_bsh_read_as_bhs = update_out_and_lse(None, None, block_out, canonical, lse_layout="bhs")
|
||||||
|
assert not torch.equal(lse_if_bsh_read_as_bhs.squeeze(-1), canonical)
|
||||||
|
assert torch.equal(lse_if_bsh_read_as_bhs.squeeze(-1), bhs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"invalid_lse",
|
||||||
|
[
|
||||||
|
torch.randn(2, 5),
|
||||||
|
torch.randn(2, 4, 4),
|
||||||
|
torch.randn(2, 3, 5, 2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_ring_lse_unknown_layout_is_rejected(invalid_lse):
|
||||||
|
block_out = torch.randn(2, 5, 3, 4)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Ring LSE"):
|
||||||
|
update_out_and_lse(None, None, block_out, invalid_lse, lse_layout="bhs")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ring_accumulation_rejects_mismatched_block_output():
|
||||||
|
out = torch.randn(2, 5, 3, 4)
|
||||||
|
lse = torch.randn(2, 5, 3, 1)
|
||||||
|
block_out = torch.randn(2, 6, 3, 4)
|
||||||
|
block_lse = torch.randn(2, 3, 6)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="block output shape"):
|
||||||
|
update_out_and_lse(out, lse, block_out, block_lse, lse_layout="bhs")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ring_accumulation_matches_logsumexp_weighting():
|
||||||
|
first_out = torch.zeros(1, 2, 1, 1)
|
||||||
|
second_out = torch.ones(1, 2, 1, 1)
|
||||||
|
first_lse = torch.zeros(1, 1, 2)
|
||||||
|
second_lse = torch.zeros(1, 1, 2)
|
||||||
|
|
||||||
|
out, lse = update_out_and_lse(None, None, first_out, first_lse, lse_layout="bhs")
|
||||||
|
out, lse = update_out_and_lse(out, lse, second_out, second_lse, lse_layout="bhs")
|
||||||
|
|
||||||
|
assert torch.allclose(out, torch.full_like(out, 0.5))
|
||||||
|
assert torch.allclose(lse, torch.full_like(lse, torch.log(torch.tensor(2.0))))
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sage_attention_rejects_mask_instead_of_ignoring_it(monkeypatch):
|
||||||
|
fake_package = types.ModuleType("sageattention")
|
||||||
|
fake_package.sageattn = lambda *args, **kwargs: pytest.fail("unexpected Sage kernel call")
|
||||||
|
monkeypatch.setitem(sys.modules, "sageattention", fake_package)
|
||||||
|
module_name = "vllm_omni.diffusion.attention.backends.sage_attn"
|
||||||
|
sys.modules.pop(module_name, None)
|
||||||
|
try:
|
||||||
|
backend_module = importlib.import_module(module_name)
|
||||||
|
impl = backend_module.SageAttentionImpl(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
softmax_scale=1.0 / 8.0,
|
||||||
|
causal=False,
|
||||||
|
)
|
||||||
|
query = torch.randn(1, 2, 4, 64)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.ones(1, 2, dtype=torch.bool))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not support attn_mask"):
|
||||||
|
impl.forward_cuda(query, query, query, metadata)
|
||||||
|
finally:
|
||||||
|
sys.modules.pop(module_name, None)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
@@ -9,6 +9,8 @@ import pytest
|
|||||||
import torch
|
import torch
|
||||||
from vllm.platforms import current_platform
|
from vllm.platforms import current_platform
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ SAGE_ATTN3_MODULE = "vllm_omni.diffusion.attention.backends.sage_attn3"
|
|||||||
|
|
||||||
def load_sage_attn3_module(monkeypatch: pytest.MonkeyPatch, kernel_impl):
|
def load_sage_attn3_module(monkeypatch: pytest.MonkeyPatch, kernel_impl):
|
||||||
fake_module = types.ModuleType("sageattn3")
|
fake_module = types.ModuleType("sageattn3")
|
||||||
fake_module.sageattn3_blackwell = kernel_impl
|
setattr(fake_module, "sageattn3_blackwell", kernel_impl)
|
||||||
monkeypatch.setitem(sys.modules, "sageattn3", fake_module)
|
monkeypatch.setitem(sys.modules, "sageattn3", fake_module)
|
||||||
sys.modules.pop(SAGE_ATTN3_MODULE, None)
|
sys.modules.pop(SAGE_ATTN3_MODULE, None)
|
||||||
return importlib.import_module(SAGE_ATTN3_MODULE)
|
return importlib.import_module(SAGE_ATTN3_MODULE)
|
||||||
@@ -51,21 +53,11 @@ def test_sage_attn3_forward_uses_blackwell_layout(monkeypatch: pytest.MonkeyPatc
|
|||||||
assert torch.allclose(output, expected)
|
assert torch.allclose(output, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_sage_attn3_falls_back_to_sdpa_for_gqa(monkeypatch: pytest.MonkeyPatch):
|
def test_sage_attn3_rejects_gqa_instead_of_falling_back(monkeypatch: pytest.MonkeyPatch):
|
||||||
def fake_kernel(*args, **kwargs):
|
def fake_kernel(*args, **kwargs):
|
||||||
raise AssertionError("sageattn3_blackwell should not be used for GQA")
|
raise AssertionError("sageattn3_blackwell should not be used for GQA")
|
||||||
|
|
||||||
backend_module = load_sage_attn3_module(monkeypatch, fake_kernel)
|
backend_module = load_sage_attn3_module(monkeypatch, fake_kernel)
|
||||||
sdpa_calls = {}
|
|
||||||
|
|
||||||
def fake_sdpa(query, key, value, **kwargs):
|
|
||||||
sdpa_calls["query_shape"] = query.shape
|
|
||||||
sdpa_calls["key_shape"] = key.shape
|
|
||||||
sdpa_calls["enable_gqa"] = kwargs["enable_gqa"]
|
|
||||||
return query + 1
|
|
||||||
|
|
||||||
monkeypatch.setattr(backend_module.F, "scaled_dot_product_attention", fake_sdpa)
|
|
||||||
|
|
||||||
impl = backend_module.SageAttention3Impl(
|
impl = backend_module.SageAttention3Impl(
|
||||||
num_heads=4,
|
num_heads=4,
|
||||||
head_size=64,
|
head_size=64,
|
||||||
@@ -77,13 +69,38 @@ def test_sage_attn3_falls_back_to_sdpa_for_gqa(monkeypatch: pytest.MonkeyPatch):
|
|||||||
key = torch.randn(2, 8, 2, 64)
|
key = torch.randn(2, 8, 2, 64)
|
||||||
value = torch.randn(2, 8, 2, 64)
|
value = torch.randn(2, 8, 2, 64)
|
||||||
|
|
||||||
output = impl.forward_cuda(query, key, value)
|
with pytest.raises(NotImplementedError, match="does not support GQA/MQA"):
|
||||||
|
impl.forward_cuda(query, key, value)
|
||||||
|
|
||||||
assert sdpa_calls["query_shape"] == (2, 4, 8, 64)
|
|
||||||
assert sdpa_calls["key_shape"] == (2, 2, 8, 64)
|
def test_sage_attn3_rejects_mask_instead_of_ignoring_it(monkeypatch: pytest.MonkeyPatch):
|
||||||
assert sdpa_calls["enable_gqa"] is True
|
def fake_kernel(*args, **kwargs):
|
||||||
expected = (query.permute(0, 2, 1, 3) + 1).permute(0, 2, 1, 3)
|
raise AssertionError("sageattn3_blackwell should not run with an unsupported mask")
|
||||||
assert torch.allclose(output, expected)
|
|
||||||
|
backend_module = load_sage_attn3_module(monkeypatch, fake_kernel)
|
||||||
|
impl = backend_module.SageAttention3Impl(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
softmax_scale=1.0 / 8.0,
|
||||||
|
causal=False,
|
||||||
|
)
|
||||||
|
query = torch.randn(2, 8, 4, 64)
|
||||||
|
metadata = AttentionMetadata(attn_mask=torch.ones(2, 8, dtype=torch.bool))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not support attn_mask"):
|
||||||
|
impl.forward_cuda(query, query, query, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sage_attn3_rejects_custom_softmax_scale(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
backend_module = load_sage_attn3_module(monkeypatch, lambda *args, **kwargs: None)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not expose a custom softmax scale"):
|
||||||
|
backend_module.SageAttention3Impl(
|
||||||
|
num_heads=4,
|
||||||
|
head_size=64,
|
||||||
|
softmax_scale=1.0,
|
||||||
|
causal=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not current_platform.is_cuda(), reason="sage_attn3 tests require CUDA platform")
|
@pytest.mark.skipif(not current_platform.is_cuda(), reason="sage_attn3 tests require CUDA platform")
|
||||||
@@ -115,10 +132,39 @@ def test_cuda_platform_selects_sage_attn3_alias(monkeypatch: pytest.MonkeyPatch)
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not current_platform.is_cuda(), reason="sage_attn3 tests require CUDA platform")
|
@pytest.mark.skipif(not current_platform.is_cuda(), reason="sage_attn3 tests require CUDA platform")
|
||||||
def test_cuda_platform_falls_back_when_sage_attn3_gpu_is_unsupported(monkeypatch: pytest.MonkeyPatch):
|
@pytest.mark.parametrize("head_size", [32, 320])
|
||||||
|
def test_cuda_platform_rejects_explicit_sage_attn3_unsupported_head_size(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, head_size: int
|
||||||
|
):
|
||||||
|
from vllm.platforms.interface import DeviceCapability
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.envs import PACKAGES_CHECKER
|
||||||
|
from vllm_omni.platforms.cuda import platform as cuda_platform_module
|
||||||
|
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
||||||
|
|
||||||
|
original_import_module = importlib.import_module
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(10, 0)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": False})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cuda_platform_module.importlib,
|
||||||
|
"import_module",
|
||||||
|
lambda module_name: object() if module_name == "sageattn3" else original_import_module(module_name),
|
||||||
|
)
|
||||||
|
load_sage_attn3_module(monkeypatch, lambda *args, **kwargs: None)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=f"head_size={head_size} is unsupported"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("SAGE_ATTN_3", head_size=head_size)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not current_platform.is_cuda(), reason="sage_attn3 tests require CUDA platform")
|
||||||
|
def test_cuda_platform_rejects_explicit_sage_attn3_on_unsupported_gpu(monkeypatch: pytest.MonkeyPatch):
|
||||||
from vllm.platforms.interface import DeviceCapability
|
from vllm.platforms.interface import DeviceCapability
|
||||||
|
|
||||||
from vllm_omni.diffusion.attention.backends.registry import DiffusionAttentionBackendEnum
|
|
||||||
from vllm_omni.diffusion.envs import PACKAGES_CHECKER
|
from vllm_omni.diffusion.envs import PACKAGES_CHECKER
|
||||||
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
||||||
|
|
||||||
@@ -129,6 +175,33 @@ def test_cuda_platform_falls_back_when_sage_attn3_gpu_is_unsupported(monkeypatch
|
|||||||
)
|
)
|
||||||
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": False})
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": False})
|
||||||
|
|
||||||
backend_path = CudaOmniPlatform.get_diffusion_attn_backend_cls("SAGE_ATTN_3", head_size=64)
|
with pytest.raises(ValueError, match="explicitly selected"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("SAGE_ATTN_3", head_size=64)
|
||||||
|
|
||||||
assert backend_path == DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
|
||||||
|
@pytest.mark.skipif(not current_platform.is_cuda(), reason="sage_attn3 tests require CUDA platform")
|
||||||
|
def test_cuda_platform_rejects_missing_explicit_sage_attn3(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
from vllm.platforms.interface import DeviceCapability
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.envs import PACKAGES_CHECKER
|
||||||
|
from vllm_omni.platforms.cuda import platform as cuda_platform_module
|
||||||
|
from vllm_omni.platforms.cuda.platform import CudaOmniPlatform
|
||||||
|
|
||||||
|
original_import_module = importlib.import_module
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CudaOmniPlatform,
|
||||||
|
"get_device_capability",
|
||||||
|
classmethod(lambda cls, device_id=0: DeviceCapability(10, 0)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(PACKAGES_CHECKER, "get_packages_info", lambda: {"has_flash_attn": False})
|
||||||
|
|
||||||
|
def _missing_sageattn3(module_name):
|
||||||
|
if module_name == "sageattn3":
|
||||||
|
raise ImportError("sageattn3 not installed")
|
||||||
|
return original_import_module(module_name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(cuda_platform_module.importlib, "import_module", _missing_sageattn3)
|
||||||
|
|
||||||
|
with pytest.raises(ImportError, match="explicitly selected"):
|
||||||
|
CudaOmniPlatform.get_diffusion_attn_backend_cls("SAGE_ATTN_3", head_size=64)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""Unit tests for diffusion attention backend selection."""
|
"""Unit tests for diffusion attention backend selection."""
|
||||||
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
@@ -100,9 +100,12 @@ def test_platform_default_used_without_resolved_spec(monkeypatch, attention_conf
|
|||||||
def test_backend_class_resolution_is_cached(monkeypatch):
|
def test_backend_class_resolution_is_cached(monkeypatch):
|
||||||
platform_calls = []
|
platform_calls = []
|
||||||
load_calls = []
|
load_calls = []
|
||||||
fake_platform = SimpleNamespace(
|
|
||||||
get_diffusion_attn_backend_cls=lambda **kwargs: (platform_calls.append(kwargs) or "fake.module.Backend")
|
def fake_get_cls(**kwargs):
|
||||||
)
|
platform_calls.append(kwargs)
|
||||||
|
return "fake.module.Backend"
|
||||||
|
|
||||||
|
fake_platform = SimpleNamespace(get_diffusion_attn_backend_cls=fake_get_cls)
|
||||||
|
|
||||||
monkeypatch.setattr("vllm_omni.platforms.current_omni_platform", fake_platform)
|
monkeypatch.setattr("vllm_omni.platforms.current_omni_platform", fake_platform)
|
||||||
|
|
||||||
@@ -126,6 +129,39 @@ def test_backend_class_resolution_is_cached(monkeypatch):
|
|||||||
assert load_calls == ["fake.module.Backend"]
|
assert load_calls == ["fake.module.Backend"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_capability_query_loads_explicit_cudnn_without_platform(monkeypatch):
|
||||||
|
platform_calls = []
|
||||||
|
|
||||||
|
def fake_get_cls(**kwargs):
|
||||||
|
platform_calls.append(kwargs)
|
||||||
|
return "fake.module.MustNotBeCalled"
|
||||||
|
|
||||||
|
fake_platform = SimpleNamespace(get_diffusion_attn_backend_cls=fake_get_cls)
|
||||||
|
monkeypatch.setattr("vllm_omni.platforms.current_omni_platform", fake_platform)
|
||||||
|
|
||||||
|
config = AttentionConfig(default=AttentionSpec(backend="CUDNN_ATTN"))
|
||||||
|
backend = selector.get_attn_backend_for_capability(role="self", attention_config=config)
|
||||||
|
|
||||||
|
assert backend.get_name() == "CUDNN_ATTN"
|
||||||
|
assert backend.supports_attention_mask()
|
||||||
|
assert platform_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_capability_query_uses_unknown_head_size_for_platform_default(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_get_backend(backend_name, head_size, allow_trtllm_default=True):
|
||||||
|
calls.append((backend_name, head_size, allow_trtllm_default))
|
||||||
|
return _PlatformBackend
|
||||||
|
|
||||||
|
monkeypatch.setattr(selector, "_cached_get_backend_cls", fake_get_backend)
|
||||||
|
|
||||||
|
backend = selector.get_attn_backend_for_capability(role="self", attention_config=AttentionConfig())
|
||||||
|
|
||||||
|
assert backend is _PlatformBackend
|
||||||
|
assert calls == [(None, selector.HEAD_SIZE_UNKNOWN, True)]
|
||||||
|
|
||||||
|
|
||||||
def test_load_backend_cls_reports_missing_module():
|
def test_load_backend_cls_reports_missing_module():
|
||||||
with pytest.raises(ImportError, match="Failed to import module missing_attention_backend"):
|
with pytest.raises(ImportError, match="Failed to import module missing_attention_backend"):
|
||||||
selector._load_backend_cls("missing_attention_backend.Backend")
|
selector._load_backend_cls("missing_attention_backend.Backend")
|
||||||
|
|||||||
+32
-17
@@ -6,7 +6,9 @@ Model specific tests for CacheDiT enablement.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -17,7 +19,8 @@ from vllm.distributed import parallel_state
|
|||||||
import vllm_omni.diffusion.cache.cachedit as cd_backend
|
import vllm_omni.diffusion.cache.cachedit as cd_backend
|
||||||
import vllm_omni.diffusion.cache.cachedit.model_specific as cd_model_specific
|
import vllm_omni.diffusion.cache.cachedit.model_specific as cd_model_specific
|
||||||
from vllm_omni.diffusion.cache.cachedit import CacheDiTAdapterConfig, CacheDiTBackend, cache_summary
|
from vllm_omni.diffusion.cache.cachedit import CacheDiTAdapterConfig, CacheDiTBackend, cache_summary
|
||||||
from vllm_omni.diffusion.data import DiffusionCacheConfig
|
from vllm_omni.diffusion.config import set_current_diffusion_config
|
||||||
|
from vllm_omni.diffusion.data import AttentionConfig, DiffusionCacheConfig
|
||||||
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import Cosmos3VFMTransformer
|
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import Cosmos3VFMTransformer
|
||||||
from vllm_omni.diffusion.models.helios.helios_transformer import HeliosTransformer3DModel
|
from vllm_omni.diffusion.models.helios.helios_transformer import HeliosTransformer3DModel
|
||||||
from vllm_omni.diffusion.models.longcat_image.longcat_image_transformer import LongCatImageTransformer2DModel
|
from vllm_omni.diffusion.models.longcat_image.longcat_image_transformer import LongCatImageTransformer2DModel
|
||||||
@@ -35,6 +38,17 @@ SEPARATE_CFG_TRANSFORMERS = [
|
|||||||
SAMPLE_CACHE_CONFIG = DiffusionCacheConfig()
|
SAMPLE_CACHE_CONFIG = DiffusionCacheConfig()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _force_torch_sdpa():
|
||||||
|
"""Pin TORCH_SDPA so CPU shape tests do not pick CUDA-only backends (FA3)."""
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(default="TORCH_SDPA"),
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1),
|
||||||
|
)
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
def test_custom_cache_dit_enablers_are_registered_explicitly():
|
def test_custom_cache_dit_enablers_are_registered_explicitly():
|
||||||
expected_enablers = {
|
expected_enablers = {
|
||||||
"Wan22Pipeline": cd_model_specific.enable_cache_for_wan22,
|
"Wan22Pipeline": cd_model_specific.enable_cache_for_wan22,
|
||||||
@@ -213,22 +227,23 @@ def test_ltx2_cache_dit_receives_audio_as_encoder(init_fake_tp_group):
|
|||||||
text_in = torch.full((1, seq_len, 16), 3.0)
|
text_in = torch.full((1, seq_len, 16), 3.0)
|
||||||
audio_text_in = torch.full((1, seq_len, 16), 4.0)
|
audio_text_in = torch.full((1, seq_len, 16), 4.0)
|
||||||
|
|
||||||
model = LTX2VideoTransformer3DModel(
|
with _force_torch_sdpa():
|
||||||
in_channels=16,
|
model = LTX2VideoTransformer3DModel(
|
||||||
out_channels=16,
|
in_channels=16,
|
||||||
patch_size=1,
|
out_channels=16,
|
||||||
patch_size_t=1,
|
patch_size=1,
|
||||||
num_attention_heads=2,
|
patch_size_t=1,
|
||||||
attention_head_dim=8,
|
num_attention_heads=2,
|
||||||
cross_attention_dim=16,
|
attention_head_dim=8,
|
||||||
audio_in_channels=16,
|
cross_attention_dim=16,
|
||||||
audio_out_channels=16,
|
audio_in_channels=16,
|
||||||
audio_num_attention_heads=2,
|
audio_out_channels=16,
|
||||||
audio_attention_head_dim=8,
|
audio_num_attention_heads=2,
|
||||||
audio_cross_attention_dim=16,
|
audio_attention_head_dim=8,
|
||||||
num_layers=2,
|
audio_cross_attention_dim=16,
|
||||||
caption_channels=16,
|
num_layers=2,
|
||||||
)
|
caption_channels=16,
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: This is currently using the LTX2 custom enabler, but the custom
|
# NOTE: This is currently using the LTX2 custom enabler, but the custom
|
||||||
# enablers will be consolidated after
|
# enablers will be consolidated after
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import vllm_omni.diffusion.data as diffusion_data
|
|||||||
from tests.helpers.mark import hardware_test
|
from tests.helpers.mark import hardware_test
|
||||||
from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data
|
from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data
|
||||||
from vllm_omni.diffusion.data import (
|
from vllm_omni.diffusion.data import (
|
||||||
|
AttentionConfig,
|
||||||
|
AttentionSpec,
|
||||||
DiffusionOutput,
|
DiffusionOutput,
|
||||||
DiffusionParallelConfig,
|
DiffusionParallelConfig,
|
||||||
OmniDiffusionConfig,
|
OmniDiffusionConfig,
|
||||||
@@ -141,6 +143,84 @@ class TestPipelineArgumentsHandling:
|
|||||||
assert isinstance(output.output, MockPipelineOutput)
|
assert isinstance(output.output, MockPipelineOutput)
|
||||||
assert output.output.image is stub_image
|
assert output.output.image is stub_image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_adapter(backend: str, set_attention_backend):
|
||||||
|
adapter = DiffusersAdapterPipeline(
|
||||||
|
od_config=_make_od_config(
|
||||||
|
diffusion_attention_config=AttentionConfig(default=AttentionSpec(backend=backend)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
adapter._pipeline = SimpleNamespace(
|
||||||
|
transformer=SimpleNamespace(set_attention_backend=set_attention_backend),
|
||||||
|
)
|
||||||
|
return adapter
|
||||||
|
|
||||||
|
def test_explicit_unbound_backend_is_rejected(self):
|
||||||
|
calls: list[str] = []
|
||||||
|
adapter = self._make_adapter("CUDNN_ATTN", calls.append)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="do not provide a binding"):
|
||||||
|
adapter._set_attention_backend()
|
||||||
|
|
||||||
|
assert calls == []
|
||||||
|
|
||||||
|
def test_failed_explicit_backend_does_not_fallback_to_native(self):
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
def reject_backend(backend):
|
||||||
|
calls.append(backend)
|
||||||
|
raise RuntimeError(f"{backend} unavailable")
|
||||||
|
|
||||||
|
adapter = self._make_adapter("SAGE_ATTN", reject_backend)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="explicitly selected attention backend 'SAGE_ATTN'"):
|
||||||
|
adapter._set_attention_backend()
|
||||||
|
|
||||||
|
assert calls == ["sage_hub", "sage", "sage_varlen"]
|
||||||
|
assert "native" not in calls
|
||||||
|
|
||||||
|
def test_explicit_flash_attn_3_hub_does_not_fallback_to_fa2_or_local(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(DiffusersAdapterPipeline, "_is_blackwell_gpu", lambda self: False)
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
def accept_later_backends(backend):
|
||||||
|
calls.append(backend)
|
||||||
|
if backend in ("flash_hub", "flash_varlen_hub", "flash", "flash_varlen", "_native_flash"):
|
||||||
|
return
|
||||||
|
raise RuntimeError(f"{backend} unavailable")
|
||||||
|
|
||||||
|
adapter = self._make_adapter("FLASH_ATTN_3_HUB", accept_later_backends)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="explicitly selected attention backend 'FLASH_ATTN_3_HUB'"):
|
||||||
|
adapter._set_attention_backend()
|
||||||
|
|
||||||
|
assert calls == ["_flash_3_hub", "_flash_3_varlen_hub"]
|
||||||
|
|
||||||
|
def test_explicit_flash_attn_hub_does_not_fallback_to_local(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(DiffusersAdapterPipeline, "_is_blackwell_gpu", lambda self: False)
|
||||||
|
calls: list[str] = []
|
||||||
|
|
||||||
|
def accept_local_flash(backend):
|
||||||
|
calls.append(backend)
|
||||||
|
if backend in ("flash", "flash_varlen", "_native_flash"):
|
||||||
|
return
|
||||||
|
raise RuntimeError(f"{backend} unavailable")
|
||||||
|
|
||||||
|
adapter = self._make_adapter("FLASH_ATTN_HUB", accept_local_flash)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="explicitly selected attention backend 'FLASH_ATTN_HUB'"):
|
||||||
|
adapter._set_attention_backend()
|
||||||
|
|
||||||
|
assert calls == ["flash_hub", "flash_varlen_hub"]
|
||||||
|
|
||||||
|
def test_explicit_torch_sdpa_maps_to_diffusers_native(self):
|
||||||
|
calls: list[str] = []
|
||||||
|
adapter = self._make_adapter("TORCH_SDPA", calls.append)
|
||||||
|
|
||||||
|
adapter._set_attention_backend()
|
||||||
|
|
||||||
|
assert calls == ["native"]
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"feature_id",
|
"feature_id",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -989,6 +989,89 @@ class TestDimensionValidation:
|
|||||||
# Since expected_dims=3 but tensor has 4 dims, should return original
|
# Since expected_dims=3 but tensor has 4 dims, should return original
|
||||||
assert result.shape == tensor_4d.shape
|
assert result.shape == tensor_4d.shape
|
||||||
|
|
||||||
|
def test_auto_pad_calls_attention_mask_capability(self, monkeypatch):
|
||||||
|
from vllm_omni.diffusion.attention import selector
|
||||||
|
from vllm_omni.diffusion.distributed import parallel_state
|
||||||
|
from vllm_omni.diffusion.distributed.sp_plan import SequenceParallelConfig
|
||||||
|
from vllm_omni.diffusion.hooks.sequence_parallel import SequenceParallelSplitHook
|
||||||
|
|
||||||
|
class NoMaskBackend:
|
||||||
|
@classmethod
|
||||||
|
def supports_attention_mask(cls, attention_spec=None):
|
||||||
|
del attention_spec
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name():
|
||||||
|
return "NO_MASK_BACKEND"
|
||||||
|
|
||||||
|
monkeypatch.setattr(parallel_state, "get_sequence_parallel_world_size", lambda: 2)
|
||||||
|
monkeypatch.setattr(selector, "get_attn_backend_for_capability", lambda **kwargs: NoMaskBackend)
|
||||||
|
|
||||||
|
hook = SequenceParallelSplitHook(
|
||||||
|
metadata={},
|
||||||
|
config=SequenceParallelConfig(ulysses_degree=2, ring_degree=1),
|
||||||
|
)
|
||||||
|
tensor = torch.randn(1, 3, 8)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not support attention_mask"):
|
||||||
|
hook._shard_with_auto_pad(tensor, dim=1)
|
||||||
|
|
||||||
|
def test_auto_pad_allows_explicit_cudnn(self, monkeypatch):
|
||||||
|
from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec, OmniDiffusionConfig
|
||||||
|
from vllm_omni.diffusion.distributed import parallel_state
|
||||||
|
from vllm_omni.diffusion.distributed.sp_plan import SequenceParallelConfig
|
||||||
|
from vllm_omni.diffusion.forward_context import set_forward_context
|
||||||
|
from vllm_omni.diffusion.hooks.sequence_parallel import SequenceParallelSplitHook
|
||||||
|
|
||||||
|
monkeypatch.setattr(parallel_state, "get_sequence_parallel_world_size", lambda: 2)
|
||||||
|
monkeypatch.setattr(parallel_state, "get_sequence_parallel_rank", lambda: 0)
|
||||||
|
monkeypatch.setattr(parallel_state, "get_ring_parallel_world_size", lambda: 1)
|
||||||
|
|
||||||
|
hook = SequenceParallelSplitHook(
|
||||||
|
metadata={},
|
||||||
|
config=SequenceParallelConfig(ulysses_degree=2, ring_degree=1),
|
||||||
|
)
|
||||||
|
tensor = torch.randn(1, 3, 8)
|
||||||
|
od_config = OmniDiffusionConfig(
|
||||||
|
model="test",
|
||||||
|
dtype=torch.float32,
|
||||||
|
diffusion_attention_config=AttentionConfig(default=AttentionSpec(backend="CUDNN_ATTN")),
|
||||||
|
)
|
||||||
|
|
||||||
|
with set_forward_context(omni_diffusion_config=od_config):
|
||||||
|
result = hook._shard_with_auto_pad(tensor, dim=1)
|
||||||
|
|
||||||
|
# seq_len=3 padded to 4, then sharded by world_size=2 → rank 0 gets 2.
|
||||||
|
assert result.shape == (1, 2, 8)
|
||||||
|
|
||||||
|
def test_auto_pad_rejects_explicit_flashinfer_cute_on_blackwell(self, monkeypatch):
|
||||||
|
from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec, OmniDiffusionConfig
|
||||||
|
from vllm_omni.diffusion.distributed import parallel_state
|
||||||
|
from vllm_omni.diffusion.distributed.sp_plan import SequenceParallelConfig
|
||||||
|
from vllm_omni.diffusion.forward_context import set_forward_context
|
||||||
|
from vllm_omni.diffusion.hooks.sequence_parallel import SequenceParallelSplitHook
|
||||||
|
|
||||||
|
monkeypatch.setattr(parallel_state, "get_sequence_parallel_world_size", lambda: 2)
|
||||||
|
monkeypatch.setattr(parallel_state, "get_sequence_parallel_rank", lambda: 0)
|
||||||
|
monkeypatch.setattr(parallel_state, "get_ring_parallel_world_size", lambda: 1)
|
||||||
|
monkeypatch.setattr(torch.cuda, "get_device_capability", lambda *args, **kwargs: (10, 0))
|
||||||
|
|
||||||
|
hook = SequenceParallelSplitHook(
|
||||||
|
metadata={},
|
||||||
|
config=SequenceParallelConfig(ulysses_degree=2, ring_degree=1),
|
||||||
|
)
|
||||||
|
tensor = torch.randn(1, 3, 8)
|
||||||
|
od_config = OmniDiffusionConfig(
|
||||||
|
model="test",
|
||||||
|
dtype=torch.float32,
|
||||||
|
diffusion_attention_config=AttentionConfig(default=AttentionSpec(backend="FLASHINFER_ATTN")),
|
||||||
|
)
|
||||||
|
|
||||||
|
with set_forward_context(omni_diffusion_config=od_config):
|
||||||
|
with pytest.raises(ValueError, match="does not support attention_mask"):
|
||||||
|
hook._shard_with_auto_pad(tensor, dim=1)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.cpu
|
@pytest.mark.cpu
|
||||||
class TestStrictModeSplitValidation:
|
class TestStrictModeSplitValidation:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -63,6 +63,20 @@ def _force_default_gemm(monkeypatch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _force_torch_sdpa():
|
||||||
|
"""Pin TORCH_SDPA so CPU shape tests do not pick CUDA-only backends (FA3)."""
|
||||||
|
from vllm_omni.diffusion.config import set_current_diffusion_config
|
||||||
|
from vllm_omni.diffusion.data import AttentionConfig
|
||||||
|
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(default="TORCH_SDPA"),
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1),
|
||||||
|
)
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
def _randomize_parameters(module: torch.nn.Module) -> None:
|
def _randomize_parameters(module: torch.nn.Module) -> None:
|
||||||
"""Fill parameters with small random values.
|
"""Fill parameters with small random values.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
@@ -182,7 +182,8 @@ def _fake_ltx_parallel_attention():
|
|||||||
return "FLASH_ATTN"
|
return "FLASH_ATTN"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def supports_attention_mask():
|
def supports_attention_mask(attention_spec=None):
|
||||||
|
del attention_spec
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def run(name):
|
def run(name):
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from collections.abc import Callable, Iterator
|
from collections.abc import Callable, Iterator, Mapping
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -81,6 +82,59 @@ def model_output_dir(parent_dir: Path, model: str) -> Path:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_DEVICE_PROFILE = "default"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SimilarityThresholds:
|
||||||
|
"""SSIM/PSNR floors for accuracy comparisons."""
|
||||||
|
|
||||||
|
ssim: float
|
||||||
|
psnr: float
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_device_profile(
|
||||||
|
device_name: str | None = None,
|
||||||
|
*,
|
||||||
|
profiles: Mapping[str, Any],
|
||||||
|
) -> str:
|
||||||
|
"""Map a device marketing name to a key in *profiles*.
|
||||||
|
|
||||||
|
When *device_name* is omitted, uses ``current_omni_platform.get_device_name()``.
|
||||||
|
Non-``default`` keys are matched as substrings; unmatched devices use ``default``.
|
||||||
|
"""
|
||||||
|
if device_name is None:
|
||||||
|
try:
|
||||||
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
|
device_name = current_omni_platform.get_device_name()
|
||||||
|
except Exception:
|
||||||
|
return DEFAULT_DEVICE_PROFILE
|
||||||
|
|
||||||
|
if not device_name:
|
||||||
|
return DEFAULT_DEVICE_PROFILE
|
||||||
|
|
||||||
|
for profile in profiles:
|
||||||
|
if profile == DEFAULT_DEVICE_PROFILE:
|
||||||
|
continue
|
||||||
|
if profile in device_name:
|
||||||
|
return profile
|
||||||
|
return DEFAULT_DEVICE_PROFILE
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_similarity_thresholds(
|
||||||
|
table: Mapping[str, SimilarityThresholds],
|
||||||
|
device_profile: str | None = None,
|
||||||
|
*,
|
||||||
|
device_name: str | None = None,
|
||||||
|
) -> SimilarityThresholds:
|
||||||
|
"""Pick SSIM/PSNR thresholds from *table* for the current (or given) device profile."""
|
||||||
|
if DEFAULT_DEVICE_PROFILE not in table:
|
||||||
|
raise ValueError("threshold table must include a 'default' entry")
|
||||||
|
profile = device_profile or resolve_device_profile(device_name, profiles=table)
|
||||||
|
return table.get(profile, table[DEFAULT_DEVICE_PROFILE])
|
||||||
|
|
||||||
|
|
||||||
_SSIM_RE = re.compile(r"All:(?P<score>[0-9.]+)")
|
_SSIM_RE = re.compile(r"All:(?P<score>[0-9.]+)")
|
||||||
_PSNR_RE = re.compile(r"average:(?P<score>[0-9.]+)")
|
_PSNR_RE = re.compile(r"average:(?P<score>[0-9.]+)")
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
@@ -7,18 +10,26 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from tests.e2e.accuracy.helpers import assert_images_pixel_close, assert_similarity, model_output_dir
|
from tests.e2e.accuracy.helpers import (
|
||||||
|
SimilarityThresholds,
|
||||||
|
assert_images_pixel_close,
|
||||||
|
assert_similarity,
|
||||||
|
model_output_dir,
|
||||||
|
resolve_device_profile,
|
||||||
|
resolve_similarity_thresholds,
|
||||||
|
)
|
||||||
from tests.helpers.mark import hardware_test
|
from tests.helpers.mark import hardware_test
|
||||||
from tests.helpers.media import get_asset_path
|
from tests.helpers.media import get_asset_path
|
||||||
from tests.helpers.runtime import OmniServer
|
from tests.helpers.runtime import OmniServer
|
||||||
|
|
||||||
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
|
pytestmark = [pytest.mark.diffusion]
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -31,15 +42,23 @@ WIDTH = 1024
|
|||||||
PROMPT = "A brown and white dog is running on the grass."
|
PROMPT = "A brown and white dog is running on the grass."
|
||||||
MEAN_THRESHOLD = 3e-2
|
MEAN_THRESHOLD = 3e-2
|
||||||
P99_THRESHOLD = 3e-1
|
P99_THRESHOLD = 3e-1
|
||||||
SSIM_THRESHOLD = 0.97
|
|
||||||
PSNR_THRESHOLD_CUDA = 30.0
|
|
||||||
PSNR_THRESHOLD_NPU = 26.0
|
PSNR_THRESHOLD_NPU = 26.0
|
||||||
|
|
||||||
|
# Per-device SSIM/PSNR for online/offline vs baseline. Unlisted devices use ``default``.
|
||||||
|
SIMILARITY_THRESHOLDS_BY_DEVICE: dict[str, SimilarityThresholds] = {
|
||||||
|
"B200": SimilarityThresholds(ssim=0.96, psnr=28.0),
|
||||||
|
"default": SimilarityThresholds(ssim=0.97, psnr=30.0),
|
||||||
|
}
|
||||||
|
|
||||||
def _psnr_threshold() -> float:
|
|
||||||
|
def _psnr_threshold(thresholds: SimilarityThresholds | None = None) -> float:
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
return PSNR_THRESHOLD_NPU if current_omni_platform.is_npu() else PSNR_THRESHOLD_CUDA
|
if current_omni_platform.is_npu():
|
||||||
|
return PSNR_THRESHOLD_NPU
|
||||||
|
if thresholds is None:
|
||||||
|
thresholds = resolve_similarity_thresholds(SIMILARITY_THRESHOLDS_BY_DEVICE)
|
||||||
|
return thresholds.psnr
|
||||||
|
|
||||||
|
|
||||||
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
||||||
@@ -57,7 +76,7 @@ def _baseline_path() -> Path:
|
|||||||
_OFFLINE_SCRIPT = _REPO_ROOT / "examples" / "offline_inference" / "hunyuan_image3" / "end2end.py"
|
_OFFLINE_SCRIPT = _REPO_ROOT / "examples" / "offline_inference" / "hunyuan_image3" / "end2end.py"
|
||||||
|
|
||||||
# DiT-only deploy config with trust_remote_code (based on hunyuan_image3_dit.yaml).
|
# DiT-only deploy config with trust_remote_code (based on hunyuan_image3_dit.yaml).
|
||||||
_DEPLOY_CONFIG = {
|
_DEPLOY_CONFIG: dict[str, Any] = {
|
||||||
"pipeline": "hunyuan_image3_dit",
|
"pipeline": "hunyuan_image3_dit",
|
||||||
"async_chunk": False,
|
"async_chunk": False,
|
||||||
"trust_remote_code": True,
|
"trust_remote_code": True,
|
||||||
@@ -121,7 +140,7 @@ def _devices() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _write_deploy_config(path: Path) -> None:
|
def _write_deploy_config(path: Path) -> None:
|
||||||
config = copy.deepcopy(_DEPLOY_CONFIG)
|
config: dict[str, Any] = copy.deepcopy(_DEPLOY_CONFIG)
|
||||||
devices = _devices()
|
devices = _devices()
|
||||||
num_devices = len(devices.split(","))
|
num_devices = len(devices.split(","))
|
||||||
config["stages"][0]["devices"] = devices
|
config["stages"][0]["devices"] = devices
|
||||||
@@ -253,6 +272,15 @@ def _run_vllm_omni_hunyuan_image3_offline(*, model: str, deploy_config: str, out
|
|||||||
def _assert_against_baseline(image: Image.Image, label: str) -> None:
|
def _assert_against_baseline(image: Image.Image, label: str) -> None:
|
||||||
baseline_path = _baseline_path()
|
baseline_path = _baseline_path()
|
||||||
baseline_image = Image.open(baseline_path).convert("RGB")
|
baseline_image = Image.open(baseline_path).convert("RGB")
|
||||||
|
device_profile = resolve_device_profile(profiles=SIMILARITY_THRESHOLDS_BY_DEVICE)
|
||||||
|
thresholds = resolve_similarity_thresholds(SIMILARITY_THRESHOLDS_BY_DEVICE, device_profile)
|
||||||
|
psnr_threshold = _psnr_threshold(thresholds)
|
||||||
|
logger.info(
|
||||||
|
"HunyuanImage3 pixel accuracy thresholds: profile=%s ssim>=%s psnr>=%s",
|
||||||
|
device_profile,
|
||||||
|
thresholds.ssim,
|
||||||
|
psnr_threshold,
|
||||||
|
)
|
||||||
|
|
||||||
assert_images_pixel_close(
|
assert_images_pixel_close(
|
||||||
model_name=f"{MODEL_NAME} ({label} vs baseline)",
|
model_name=f"{MODEL_NAME} ({label} vs baseline)",
|
||||||
@@ -265,11 +293,12 @@ def _assert_against_baseline(image: Image.Image, label: str) -> None:
|
|||||||
model_name=f"{MODEL_NAME} ({label} vs baseline)",
|
model_name=f"{MODEL_NAME} ({label} vs baseline)",
|
||||||
vllm_image=image,
|
vllm_image=image,
|
||||||
diffusers_image=baseline_image,
|
diffusers_image=baseline_image,
|
||||||
ssim_threshold=SSIM_THRESHOLD,
|
ssim_threshold=thresholds.ssim,
|
||||||
psnr_threshold=_psnr_threshold(),
|
psnr_threshold=psnr_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.full_model
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=4)
|
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=4)
|
||||||
def test_hunyuan_image3_pixel_accuracy_online(accuracy_artifact_root: Path) -> None:
|
def test_hunyuan_image3_pixel_accuracy_online(accuracy_artifact_root: Path) -> None:
|
||||||
model = _model_name()
|
model = _model_name()
|
||||||
@@ -284,6 +313,7 @@ def test_hunyuan_image3_pixel_accuracy_online(accuracy_artifact_root: Path) -> N
|
|||||||
_assert_against_baseline(image, "online")
|
_assert_against_baseline(image, "online")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.full_model
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=4)
|
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=4)
|
||||||
def test_hunyuan_image3_pixel_accuracy_offline(accuracy_artifact_root: Path) -> None:
|
def test_hunyuan_image3_pixel_accuracy_offline(accuracy_artifact_root: Path) -> None:
|
||||||
model = _model_name()
|
model = _model_name()
|
||||||
|
|||||||
@@ -6,54 +6,24 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from base64 import b64decode
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
|
||||||
import torch
|
import torch
|
||||||
from diffusers import UniPCMultistepScheduler
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
from tests.e2e.accuracy.helpers import (
|
||||||
assert_video_metadata,
|
assert_video_metadata,
|
||||||
assert_video_similarity_metrics,
|
assert_video_similarity_metrics,
|
||||||
|
build_online_image_reference,
|
||||||
|
online_timeout_seconds,
|
||||||
|
probe_binary,
|
||||||
|
probe_video,
|
||||||
|
resolve_device_profile,
|
||||||
|
resolve_similarity_thresholds,
|
||||||
|
send_video_request_with_timeout,
|
||||||
|
validate_image_source,
|
||||||
video_artifact_dir,
|
video_artifact_dir,
|
||||||
)
|
)
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
build_online_image_reference as _build_online_image_reference,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
online_timeout_seconds as _online_timeout_seconds,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
parse_psnr_score as _parse_psnr_score,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
parse_ssim_score as _parse_ssim_score,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
parse_video_metadata as _parse_video_metadata,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
probe_binary as _probe_binary,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
probe_video as _probe_video,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
send_video_request_with_timeout as _send_video_request_with_timeout,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.helpers import (
|
|
||||||
validate_image_source as _validate_image_source,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.wan22_i2v.run_wan22_i2v_diffusers_cp import (
|
|
||||||
_configure_scheduler,
|
|
||||||
_ensure_wan_ftfy_fallback,
|
|
||||||
_IdentityFtfy,
|
|
||||||
_offline_cuda_device,
|
|
||||||
_resize_to_target,
|
|
||||||
)
|
|
||||||
from tests.e2e.accuracy.wan22_i2v.wan22_i2v_video_similarity_common import (
|
from tests.e2e.accuracy.wan22_i2v.wan22_i2v_video_similarity_common import (
|
||||||
FLOW_SHIFT,
|
FLOW_SHIFT,
|
||||||
FPS,
|
FPS,
|
||||||
@@ -65,178 +35,16 @@ from tests.e2e.accuracy.wan22_i2v.wan22_i2v_video_similarity_common import (
|
|||||||
NUM_FRAMES,
|
NUM_FRAMES,
|
||||||
NUM_INFERENCE_STEPS,
|
NUM_INFERENCE_STEPS,
|
||||||
PROMPT,
|
PROMPT,
|
||||||
PSNR_THRESHOLD,
|
|
||||||
RABBIT_IMAGE_URL,
|
RABBIT_IMAGE_URL,
|
||||||
SEED,
|
SEED,
|
||||||
|
SIMILARITY_THRESHOLDS_BY_DEVICE,
|
||||||
SIZE,
|
SIZE,
|
||||||
SSIM_THRESHOLD,
|
|
||||||
WIDTH,
|
WIDTH,
|
||||||
)
|
)
|
||||||
from tests.helpers.mark import hardware_test
|
from tests.helpers.mark import hardware_test
|
||||||
from tests.helpers.runtime import OmniServerParams
|
from tests.helpers.runtime import OmniServerParams
|
||||||
|
|
||||||
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
|
pytestmark = [pytest.mark.diffusion]
|
||||||
|
|
||||||
|
|
||||||
def test_parse_video_metadata_extracts_dimensions_and_fps() -> None:
|
|
||||||
payload = {
|
|
||||||
"streams": [
|
|
||||||
{
|
|
||||||
"width": 832,
|
|
||||||
"height": 480,
|
|
||||||
"avg_frame_rate": "16/1",
|
|
||||||
"nb_read_frames": "93",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata = _parse_video_metadata(payload)
|
|
||||||
|
|
||||||
assert metadata["width"] == 832
|
|
||||||
assert metadata["height"] == 480
|
|
||||||
assert metadata["fps"] == 16.0
|
|
||||||
assert metadata["frame_count"] == 93
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ssim_summary_extracts_all_score() -> None:
|
|
||||||
output = """
|
|
||||||
[Parsed_ssim_0 @ 000001] SSIM Y:0.971903 (15.512007) U:0.965077 (14.569044) V:0.962414 (14.252637) All:0.968311 (15.035654)
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert _parse_ssim_score(output) == 0.968311
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_psnr_summary_extracts_average_score() -> None:
|
|
||||||
output = """
|
|
||||||
[Parsed_psnr_0 @ 000001] PSNR y:32.670157 u:31.844621 v:31.513839 average:32.148004 min:31.563744 max:33.201457
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert _parse_psnr_score(output) == 32.148004
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_diffusers_command_uses_python_runner_path(tmp_path: Path) -> None:
|
|
||||||
runner_path = tmp_path / "run_wan22_i2v_diffusers_cp.py"
|
|
||||||
command = _build_diffusers_command(
|
|
||||||
runner_path=runner_path,
|
|
||||||
image_source=RABBIT_IMAGE_URL,
|
|
||||||
output_path=tmp_path / "offline.mp4",
|
|
||||||
metadata_path=tmp_path / "offline.json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert command[:2] == [
|
|
||||||
sys.executable,
|
|
||||||
str(runner_path),
|
|
||||||
]
|
|
||||||
assert "--output" in command
|
|
||||||
assert "--metadata-output" in command
|
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_image_source_prefers_existing_local_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
image_path = tmp_path / "rabbit.png"
|
|
||||||
Image.new("RGB", (8, 8), color=(255, 128, 64)).save(image_path)
|
|
||||||
|
|
||||||
assert _resolve_image_source(str(image_path)) == str(image_path.resolve())
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_online_image_reference_uses_data_url_for_local_path(tmp_path: Path) -> None:
|
|
||||||
image_path = tmp_path / "rabbit.png"
|
|
||||||
Image.new("RGB", (4, 2), color=(10, 20, 30)).save(image_path)
|
|
||||||
|
|
||||||
reference = _build_online_image_reference(str(image_path))
|
|
||||||
|
|
||||||
assert reference.startswith("data:image/png;base64,")
|
|
||||||
encoded = reference.split(",", 1)[1]
|
|
||||||
assert len(b64decode(encoded)) > 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_video_request_with_timeout_uses_requested_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
class DummyResponse:
|
|
||||||
def raise_for_status(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def json(self) -> dict[str, str]:
|
|
||||||
return {"id": "video_123"}
|
|
||||||
|
|
||||||
class DummyClient:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.run_level = "core_model"
|
|
||||||
self.wait_args: tuple[str, int, int] | None = None
|
|
||||||
|
|
||||||
def _build_url(self, path: str) -> str:
|
|
||||||
return f"http://localhost:8000{path}"
|
|
||||||
|
|
||||||
def _wait_until_video_completed(
|
|
||||||
self,
|
|
||||||
video_id: str,
|
|
||||||
poll_interval_seconds: int = 2,
|
|
||||||
timeout_seconds: int = 300,
|
|
||||||
) -> None:
|
|
||||||
self.wait_args = (video_id, poll_interval_seconds, timeout_seconds)
|
|
||||||
|
|
||||||
def _download_video_content(self, video_id: str) -> bytes:
|
|
||||||
return b"video-bytes"
|
|
||||||
|
|
||||||
monkeypatch.setattr(requests, "post", lambda *args, **kwargs: DummyResponse())
|
|
||||||
|
|
||||||
client = DummyClient()
|
|
||||||
result = _send_video_request_with_timeout(
|
|
||||||
client,
|
|
||||||
{
|
|
||||||
"form_data": {"prompt": "test"},
|
|
||||||
"image_reference": RABBIT_IMAGE_URL,
|
|
||||||
},
|
|
||||||
timeout_seconds=1200,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result == b"video-bytes"
|
|
||||||
assert client.wait_args == ("video_123", 2, 1200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_online_timeout_defaults_to_1200() -> None:
|
|
||||||
assert _online_timeout_seconds(None) == 1200
|
|
||||||
|
|
||||||
|
|
||||||
def test_artifact_dir_is_under_repo_result_folder(tmp_path: Path) -> None:
|
|
||||||
artifact_dir = _artifact_dir(str(tmp_path / "rabbit.png"))
|
|
||||||
|
|
||||||
assert artifact_dir.parent == Path(__file__).parent / "result"
|
|
||||||
assert artifact_dir.name.startswith("rabbit-")
|
|
||||||
|
|
||||||
|
|
||||||
def test_offline_cuda_device_uses_indexed_cuda_device() -> None:
|
|
||||||
assert _offline_cuda_device() == torch.device("cuda:0")
|
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_wan_ftfy_fallback_sets_identity(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
from diffusers.pipelines.wan import pipeline_wan_i2v as wan_i2v_module
|
|
||||||
|
|
||||||
monkeypatch.delattr(wan_i2v_module, "ftfy", raising=False)
|
|
||||||
_ensure_wan_ftfy_fallback()
|
|
||||||
|
|
||||||
assert hasattr(wan_i2v_module, "ftfy")
|
|
||||||
assert isinstance(wan_i2v_module.ftfy, _IdentityFtfy)
|
|
||||||
assert wan_i2v_module.ftfy.fix_text("abc") == "abc"
|
|
||||||
|
|
||||||
|
|
||||||
def test_resize_to_target_matches_requested_dimensions() -> None:
|
|
||||||
image = Image.new("RGB", (100, 50), color=(1, 2, 3))
|
|
||||||
|
|
||||||
resized = _resize_to_target(image, width=832, height=480)
|
|
||||||
|
|
||||||
assert resized.size == (832, 480)
|
|
||||||
|
|
||||||
|
|
||||||
def test_configure_scheduler_switches_to_unipc() -> None:
|
|
||||||
class DummyPipe:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.scheduler = UniPCMultistepScheduler(num_train_timesteps=1000, flow_shift=1.0)
|
|
||||||
|
|
||||||
pipe = DummyPipe()
|
|
||||||
_configure_scheduler(pipe, flow_shift=5.0)
|
|
||||||
|
|
||||||
assert isinstance(pipe.scheduler, UniPCMultistepScheduler)
|
|
||||||
assert pipe.scheduler.config.flow_shift == 5.0
|
|
||||||
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||||
WORKSPACE_ROOT = REPO_ROOT.parent
|
WORKSPACE_ROOT = REPO_ROOT.parent
|
||||||
@@ -312,7 +120,7 @@ def _runner_env() -> dict[str, str]:
|
|||||||
if existing_pythonpath:
|
if existing_pythonpath:
|
||||||
pythonpath_parts.append(existing_pythonpath)
|
pythonpath_parts.append(existing_pythonpath)
|
||||||
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
|
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
|
||||||
env.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
@@ -324,12 +132,8 @@ def _resolve_image_source(configured: str | None) -> str:
|
|||||||
return configured
|
return configured
|
||||||
|
|
||||||
|
|
||||||
def _artifact_dir(image_source: str) -> Path:
|
|
||||||
return video_artifact_dir(RESULT_ROOT, image_source)
|
|
||||||
|
|
||||||
|
|
||||||
def _artifact_paths(image_source: str) -> tuple[Path, Path, Path]:
|
def _artifact_paths(image_source: str) -> tuple[Path, Path, Path]:
|
||||||
artifact_dir = _artifact_dir(image_source)
|
artifact_dir = video_artifact_dir(RESULT_ROOT, image_source)
|
||||||
artifact_dir.mkdir(parents=True, exist_ok=True)
|
artifact_dir.mkdir(parents=True, exist_ok=True)
|
||||||
return (
|
return (
|
||||||
artifact_dir / "online.mp4",
|
artifact_dir / "online.mp4",
|
||||||
@@ -343,7 +147,7 @@ def _generate_online_video(
|
|||||||
omni_server,
|
omni_server,
|
||||||
online_client,
|
online_client,
|
||||||
image_source: str,
|
image_source: str,
|
||||||
online_timeout_seconds: int,
|
online_timeout_seconds_value: int,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
online_path, _, _ = _artifact_paths(image_source)
|
online_path, _, _ = _artifact_paths(image_source)
|
||||||
request_config = {
|
request_config = {
|
||||||
@@ -360,12 +164,12 @@ def _generate_online_video(
|
|||||||
"num_inference_steps": NUM_INFERENCE_STEPS,
|
"num_inference_steps": NUM_INFERENCE_STEPS,
|
||||||
"seed": SEED,
|
"seed": SEED,
|
||||||
},
|
},
|
||||||
"image_reference": _build_online_image_reference(image_source),
|
"image_reference": build_online_image_reference(image_source),
|
||||||
}
|
}
|
||||||
online_video_bytes = _send_video_request_with_timeout(
|
online_video_bytes = send_video_request_with_timeout(
|
||||||
online_client,
|
online_client,
|
||||||
request_config,
|
request_config,
|
||||||
timeout_seconds=_online_timeout_seconds(online_timeout_seconds),
|
timeout_seconds=online_timeout_seconds(online_timeout_seconds_value),
|
||||||
)
|
)
|
||||||
online_path.write_bytes(online_video_bytes)
|
online_path.write_bytes(online_video_bytes)
|
||||||
return online_path
|
return online_path
|
||||||
@@ -390,6 +194,7 @@ def _generate_offline_video(*, image_source: str) -> tuple[Path, Path]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.benchmark
|
@pytest.mark.benchmark
|
||||||
|
@pytest.mark.full_model
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||||
def test_wan22_i2v_diffusers_offline_generates_video(
|
def test_wan22_i2v_diffusers_offline_generates_video(
|
||||||
wan22_i2v_image_source: str | None,
|
wan22_i2v_image_source: str | None,
|
||||||
@@ -397,20 +202,21 @@ def test_wan22_i2v_diffusers_offline_generates_video(
|
|||||||
if not torch.cuda.is_available():
|
if not torch.cuda.is_available():
|
||||||
pytest.skip("Wan2.2 I2V diffusers offline test requires CUDA.")
|
pytest.skip("Wan2.2 I2V diffusers offline test requires CUDA.")
|
||||||
|
|
||||||
_probe_binary("ffprobe")
|
probe_binary("ffprobe")
|
||||||
if not RUNNER_PATH.exists():
|
if not RUNNER_PATH.exists():
|
||||||
raise AssertionError(f"Offline diffusers runner does not exist: {RUNNER_PATH}")
|
raise AssertionError(f"Offline diffusers runner does not exist: {RUNNER_PATH}")
|
||||||
|
|
||||||
image_source = _resolve_image_source(wan22_i2v_image_source)
|
image_source = _resolve_image_source(wan22_i2v_image_source)
|
||||||
_validate_image_source(image_source)
|
validate_image_source(image_source)
|
||||||
offline_path, offline_metadata_path = _generate_offline_video(image_source=image_source)
|
offline_path, offline_metadata_path = _generate_offline_video(image_source=image_source)
|
||||||
assert offline_path.exists(), f"Expected offline video artifact at {offline_path}"
|
assert offline_path.exists(), f"Expected offline video artifact at {offline_path}"
|
||||||
assert offline_metadata_path.exists(), f"Expected offline metadata artifact at {offline_metadata_path}"
|
assert offline_metadata_path.exists(), f"Expected offline metadata artifact at {offline_metadata_path}"
|
||||||
offline_metadata = _probe_video(offline_path)
|
offline_metadata = probe_video(offline_path)
|
||||||
assert_video_metadata(offline_metadata, width=WIDTH, height=HEIGHT, fps=FPS, frame_count=NUM_FRAMES)
|
assert_video_metadata(offline_metadata, width=WIDTH, height=HEIGHT, fps=FPS, frame_count=NUM_FRAMES)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.benchmark
|
@pytest.mark.benchmark
|
||||||
|
@pytest.mark.full_model
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", SERVER_CASES, indirect=True)
|
@pytest.mark.parametrize("omni_server", SERVER_CASES, indirect=True)
|
||||||
def test_wan22_i2v_online_serving_generates_video(
|
def test_wan22_i2v_online_serving_generates_video(
|
||||||
@@ -422,21 +228,22 @@ def test_wan22_i2v_online_serving_generates_video(
|
|||||||
if not torch.cuda.is_available() or torch.accelerator.device_count() < 2:
|
if not torch.cuda.is_available() or torch.accelerator.device_count() < 2:
|
||||||
pytest.skip("Wan2.2 I2V similarity e2e test requires >= 2 CUDA GPUs.")
|
pytest.skip("Wan2.2 I2V similarity e2e test requires >= 2 CUDA GPUs.")
|
||||||
|
|
||||||
_probe_binary("ffprobe")
|
probe_binary("ffprobe")
|
||||||
image_source = _resolve_image_source(wan22_i2v_image_source)
|
image_source = _resolve_image_source(wan22_i2v_image_source)
|
||||||
_validate_image_source(image_source)
|
validate_image_source(image_source)
|
||||||
online_path = _generate_online_video(
|
online_path = _generate_online_video(
|
||||||
omni_server=omni_server,
|
omni_server=omni_server,
|
||||||
online_client=online_client,
|
online_client=online_client,
|
||||||
image_source=image_source,
|
image_source=image_source,
|
||||||
online_timeout_seconds=wan22_i2v_online_timeout_seconds,
|
online_timeout_seconds_value=wan22_i2v_online_timeout_seconds,
|
||||||
)
|
)
|
||||||
assert online_path.exists(), f"Expected online video artifact at {online_path}"
|
assert online_path.exists(), f"Expected online video artifact at {online_path}"
|
||||||
online_metadata = _probe_video(online_path)
|
online_metadata = probe_video(online_path)
|
||||||
assert_video_metadata(online_metadata, width=WIDTH, height=HEIGHT, fps=FPS, frame_count=NUM_FRAMES)
|
assert_video_metadata(online_metadata, width=WIDTH, height=HEIGHT, fps=FPS, frame_count=NUM_FRAMES)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.benchmark
|
@pytest.mark.benchmark
|
||||||
|
@pytest.mark.full_model
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||||
def test_wan22_i2v_serving_matches_diffusers_video_similarity(
|
def test_wan22_i2v_serving_matches_diffusers_video_similarity(
|
||||||
wan22_i2v_image_source: str | None,
|
wan22_i2v_image_source: str | None,
|
||||||
@@ -444,13 +251,13 @@ def test_wan22_i2v_serving_matches_diffusers_video_similarity(
|
|||||||
if not torch.cuda.is_available() or torch.accelerator.device_count() < 2:
|
if not torch.cuda.is_available() or torch.accelerator.device_count() < 2:
|
||||||
pytest.skip("Wan2.2 I2V similarity e2e test requires >= 2 CUDA GPUs.")
|
pytest.skip("Wan2.2 I2V similarity e2e test requires >= 2 CUDA GPUs.")
|
||||||
|
|
||||||
_probe_binary("ffmpeg")
|
probe_binary("ffmpeg")
|
||||||
_probe_binary("ffprobe")
|
probe_binary("ffprobe")
|
||||||
if not RUNNER_PATH.exists():
|
if not RUNNER_PATH.exists():
|
||||||
raise AssertionError(f"Offline diffusers runner does not exist: {RUNNER_PATH}")
|
raise AssertionError(f"Offline diffusers runner does not exist: {RUNNER_PATH}")
|
||||||
|
|
||||||
image_source = _resolve_image_source(wan22_i2v_image_source)
|
image_source = _resolve_image_source(wan22_i2v_image_source)
|
||||||
_validate_image_source(image_source)
|
validate_image_source(image_source)
|
||||||
online_path, offline_path, offline_metadata_path = _artifact_paths(image_source)
|
online_path, offline_path, offline_metadata_path = _artifact_paths(image_source)
|
||||||
|
|
||||||
if not online_path.exists():
|
if not online_path.exists():
|
||||||
@@ -462,8 +269,8 @@ def test_wan22_i2v_serving_matches_diffusers_video_similarity(
|
|||||||
assert offline_path.exists(), f"Expected offline video artifact at {offline_path}"
|
assert offline_path.exists(), f"Expected offline video artifact at {offline_path}"
|
||||||
assert offline_metadata_path.exists(), f"Expected offline metadata artifact at {offline_metadata_path}"
|
assert offline_metadata_path.exists(), f"Expected offline metadata artifact at {offline_metadata_path}"
|
||||||
|
|
||||||
online_metadata = _probe_video(online_path)
|
online_metadata = probe_video(online_path)
|
||||||
offline_metadata = _probe_video(offline_path)
|
offline_metadata = probe_video(offline_path)
|
||||||
assert online_metadata == offline_metadata, (
|
assert online_metadata == offline_metadata, (
|
||||||
f"Video metadata mismatch:\n"
|
f"Video metadata mismatch:\n"
|
||||||
f"online={online_metadata}\n"
|
f"online={online_metadata}\n"
|
||||||
@@ -472,11 +279,17 @@ def test_wan22_i2v_serving_matches_diffusers_video_similarity(
|
|||||||
f"offline_path={offline_path}"
|
f"offline_path={offline_path}"
|
||||||
)
|
)
|
||||||
assert_video_metadata(online_metadata, width=WIDTH, height=HEIGHT, fps=FPS, frame_count=NUM_FRAMES)
|
assert_video_metadata(online_metadata, width=WIDTH, height=HEIGHT, fps=FPS, frame_count=NUM_FRAMES)
|
||||||
|
device_profile = resolve_device_profile(profiles=SIMILARITY_THRESHOLDS_BY_DEVICE)
|
||||||
|
thresholds = resolve_similarity_thresholds(SIMILARITY_THRESHOLDS_BY_DEVICE, device_profile)
|
||||||
|
print(
|
||||||
|
f"wan22_i2v similarity thresholds: device_profile={device_profile}, "
|
||||||
|
f"ssim>={thresholds.ssim:.6f}, psnr>={thresholds.psnr:.6f}"
|
||||||
|
)
|
||||||
assert_video_similarity_metrics(
|
assert_video_similarity_metrics(
|
||||||
label="wan22_i2v",
|
label="wan22_i2v",
|
||||||
online_path=online_path,
|
online_path=online_path,
|
||||||
offline_path=offline_path,
|
offline_path=offline_path,
|
||||||
ssim_threshold=SSIM_THRESHOLD,
|
ssim_threshold=thresholds.ssim,
|
||||||
psnr_threshold=PSNR_THRESHOLD,
|
psnr_threshold=thresholds.psnr,
|
||||||
)
|
)
|
||||||
print(f"offline_metadata={offline_metadata_path}")
|
print(f"offline_metadata={offline_metadata_path}")
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from tests.e2e.accuracy.helpers import SimilarityThresholds
|
||||||
|
|
||||||
RABBIT_IMAGE_URL = "https://vllm-public-assets.s3.us-west-2.amazonaws.com/omni-assets/rabbit.png"
|
RABBIT_IMAGE_URL = "https://vllm-public-assets.s3.us-west-2.amazonaws.com/omni-assets/rabbit.png"
|
||||||
MODEL_NAME = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
|
MODEL_NAME = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
|
||||||
SIZE = "832x480"
|
SIZE = "832x480"
|
||||||
@@ -13,8 +15,16 @@ FLOW_SHIFT = 5.0
|
|||||||
NUM_INFERENCE_STEPS = 50
|
NUM_INFERENCE_STEPS = 50
|
||||||
SEED = 42
|
SEED = 42
|
||||||
BOUNDARY_RATIO = 0.875
|
BOUNDARY_RATIO = 0.875
|
||||||
SSIM_THRESHOLD = 0.94
|
|
||||||
PSNR_THRESHOLD = 28.0
|
|
||||||
|
# Per-device overrides for online-vs-diffusers video similarity.
|
||||||
|
# Only listed profiles are matched against ``get_device_name()``; everything
|
||||||
|
# else (including H100/L4) uses ``default``.
|
||||||
|
SIMILARITY_THRESHOLDS_BY_DEVICE: dict[str, SimilarityThresholds] = {
|
||||||
|
"B200": SimilarityThresholds(ssim=0.93, psnr=28.0),
|
||||||
|
"default": SimilarityThresholds(ssim=0.94, psnr=28.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
PROMPT = """一只棕色野兔的正面特写镜头,采用低角度仰拍视角,营造亲密而庄严的视觉冲击。兔子一双圆润漆黑的大眼睛直视镜头深处,眼神中交织着野生动物的警觉与一丝难以言喻的温柔好奇,仿佛在与观者建立跨越物种的静默对话。它毛色呈现层次丰富的棕褐渐变,从浅奶油色腹部过渡到深棕背部,每根毛发纹理清晰可辨,在侧光下泛着丝绸般的光泽。细长洁白的胡须共有三对,随呼吸节奏微微颤动,偶尔因捕捉气流信息而轻轻摇摆。
|
PROMPT = """一只棕色野兔的正面特写镜头,采用低角度仰拍视角,营造亲密而庄严的视觉冲击。兔子一双圆润漆黑的大眼睛直视镜头深处,眼神中交织着野生动物的警觉与一丝难以言喻的温柔好奇,仿佛在与观者建立跨越物种的静默对话。它毛色呈现层次丰富的棕褐渐变,从浅奶油色腹部过渡到深棕背部,每根毛发纹理清晰可辨,在侧光下泛着丝绸般的光泽。细长洁白的胡须共有三对,随呼吸节奏微微颤动,偶尔因捕捉气流信息而轻轻摇摆。
|
||||||
两只标志性的长耳完全竖立,耳廓外侧覆盖短密棕毛,内侧则露出粉嫩的血管网络,薄如蝉翼的皮肤下血液流动隐约可见,耳朵以细微幅度不时转动,精准定位声源方向。背景是一片澄澈的蔚蓝天空,形态蓬松的白色积云以缓慢速度横向漂移,云影在兔子头顶交替变化,光线随之明暗流转。晴朗天气的明媚阳光从画面左上方45度角倾泻而下,在兔脸右侧形成柔和的伦勃朗式阴影,强化了面部立体感和皮毛质感。
|
两只标志性的长耳完全竖立,耳廓外侧覆盖短密棕毛,内侧则露出粉嫩的血管网络,薄如蝉翼的皮肤下血液流动隐约可见,耳朵以细微幅度不时转动,精准定位声源方向。背景是一片澄澈的蔚蓝天空,形态蓬松的白色积云以缓慢速度横向漂移,云影在兔子头顶交替变化,光线随之明暗流转。晴朗天气的明媚阳光从画面左上方45度角倾泻而下,在兔脸右侧形成柔和的伦勃朗式阴影,强化了面部立体感和皮毛质感。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""E2E offline inference tests for the Gepard-1.0 single-stage native-AR TTS.
|
"""E2E offline inference tests for the Gepard-1.0 single-stage native-AR TTS.
|
||||||
|
|
||||||
Zero-shot only (the model's default learned voice). These tests need a GPU and
|
Zero-shot only (the model's default learned voice). These tests need a GPU and
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
"""
|
"""
|
||||||
E2E offline tests for MiniCPM-o 4.5 model with multimodal input and audio / text output.
|
E2E offline tests for MiniCPM-o 4.5 model with multimodal input and audio / text output.
|
||||||
"""
|
"""
|
||||||
@@ -44,7 +47,7 @@ def test_text_to_text(omni_runner, omni_runner_handler) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||||
def test_audio_to_text(omni_runner, omni_runner_handler) -> None:
|
def test_audio_to_text(omni_runner, omni_runner_handler) -> None:
|
||||||
"""Test processing audio, generating text output."""
|
"""Test processing audio, generating text output."""
|
||||||
@@ -57,7 +60,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||||
def test_image_to_text(omni_runner, omni_runner_handler) -> None:
|
def test_image_to_text(omni_runner, omni_runner_handler) -> None:
|
||||||
"""Test processing image, generating text output."""
|
"""Test processing image, generating text output."""
|
||||||
@@ -68,7 +71,7 @@ def test_image_to_text(omni_runner, omni_runner_handler) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||||
def test_video_to_text(omni_runner, omni_runner_handler) -> None:
|
def test_video_to_text(omni_runner, omni_runner_handler) -> None:
|
||||||
"""Test processing video, generating text output."""
|
"""Test processing video, generating text output."""
|
||||||
@@ -79,7 +82,7 @@ def test_video_to_text(omni_runner, omni_runner_handler) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||||
def test_text_to_audio(omni_runner, omni_runner_handler) -> None:
|
def test_text_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||||
"""Test processing text and generating audio through Talker and Code2Wav."""
|
"""Test processing text and generating audio through Talker and Code2Wav."""
|
||||||
@@ -109,7 +112,7 @@ def test_mix_to_audio(omni_runner, omni_runner_handler) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||||
def test_video_to_audio(omni_runner, omni_runner_handler) -> None:
|
def test_video_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||||
"""Test processing video, generating audio output."""
|
"""Test processing video, generating audio output."""
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ quant_params = [(QUANTIZED_MODEL, stage_config)]
|
|||||||
|
|
||||||
|
|
||||||
@_SKIP_ISSUE_5652
|
@_SKIP_ISSUE_5652
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||||
def test_text_to_text(omni_runner, offline_client):
|
def test_text_to_text(omni_runner, offline_client):
|
||||||
"""Text input → text output with W4A16 quantized Qwen3-Omni."""
|
"""Text input → text output with W4A16 quantized Qwen3-Omni."""
|
||||||
@@ -97,7 +97,7 @@ def test_text_to_text(omni_runner, offline_client):
|
|||||||
|
|
||||||
|
|
||||||
@_SKIP_ISSUE_5652
|
@_SKIP_ISSUE_5652
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||||
def test_audio_to_text(omni_runner, offline_client):
|
def test_audio_to_text(omni_runner, offline_client):
|
||||||
"""Audio input → text output with W4A16 quantized Qwen3-Omni."""
|
"""Audio input → text output with W4A16 quantized Qwen3-Omni."""
|
||||||
@@ -121,7 +121,7 @@ def test_audio_to_text(omni_runner, offline_client):
|
|||||||
|
|
||||||
|
|
||||||
@_SKIP_ISSUE_5652
|
@_SKIP_ISSUE_5652
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||||
def test_image_to_text(omni_runner, offline_client):
|
def test_image_to_text(omni_runner, offline_client):
|
||||||
"""Image input → text output with W4A16 quantized Qwen3-Omni."""
|
"""Image input → text output with W4A16 quantized Qwen3-Omni."""
|
||||||
@@ -143,7 +143,7 @@ def test_image_to_text(omni_runner, offline_client):
|
|||||||
|
|
||||||
|
|
||||||
@_SKIP_ISSUE_5652
|
@_SKIP_ISSUE_5652
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||||
def test_video_to_text(omni_runner, offline_client):
|
def test_video_to_text(omni_runner, offline_client):
|
||||||
"""Video input → text output with W4A16 quantized Qwen3-Omni."""
|
"""Video input → text output with W4A16 quantized Qwen3-Omni."""
|
||||||
@@ -165,7 +165,7 @@ def test_video_to_text(omni_runner, offline_client):
|
|||||||
|
|
||||||
|
|
||||||
@_SKIP_ISSUE_5652
|
@_SKIP_ISSUE_5652
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||||
def test_video_to_audio(omni_runner, offline_client):
|
def test_video_to_audio(omni_runner, offline_client):
|
||||||
"""Video input → audio output with W4A16 quantized Qwen3-Omni."""
|
"""Video input → audio output with W4A16 quantized Qwen3-Omni."""
|
||||||
@@ -186,7 +186,7 @@ def test_video_to_audio(omni_runner, offline_client):
|
|||||||
|
|
||||||
|
|
||||||
@_SKIP_ISSUE_5652
|
@_SKIP_ISSUE_5652
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||||
def test_mix_to_audio(omni_runner, offline_client):
|
def test_mix_to_audio(omni_runner, offline_client):
|
||||||
"""Mixed-modality input → audio output with W4A16 quantized Qwen3-Omni."""
|
"""Mixed-modality input → audio output with W4A16 quantized Qwen3-Omni."""
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def _stage_config():
|
|||||||
quant_params = [(QUANTIZED_MODEL, _stage_config())]
|
quant_params = [(QUANTIZED_MODEL, _stage_config())]
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||||
def test_marlin_fallback_no_nan_collapse(omni_runner, offline_client):
|
def test_marlin_fallback_no_nan_collapse(omni_runner, offline_client):
|
||||||
"""H100 Marlin FP4 fallback: text in -> coherent text out.
|
"""H100 Marlin FP4 fallback: text in -> coherent text out.
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ def test_text_to_text_001(omni_server, openai_client) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_text_to_audio_001(omni_server, openai_client) -> None:
|
def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -223,7 +223,7 @@ def test_text_to_audio_with_default_reference(omni_server, openai_client) -> Non
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_audio_to_text_audio_001(omni_server, openai_client) -> None:
|
def test_audio_to_text_audio_001(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -254,7 +254,7 @@ def test_audio_to_text_audio_001(omni_server, openai_client) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_image_to_text_audio_001(omni_server, openai_client) -> None:
|
def test_image_to_text_audio_001(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -283,7 +283,7 @@ def test_image_to_text_audio_001(omni_server, openai_client) -> None:
|
|||||||
|
|
||||||
@pytest.mark.full_model
|
@pytest.mark.full_model
|
||||||
@pytest.mark.omni
|
@pytest.mark.omni
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_video_to_text_audio_001(omni_server, openai_client) -> None:
|
def test_video_to_text_audio_001(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ _TTS_EXTRA_BODY = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_text_video_to_text_001(omni_server, openai_client) -> None:
|
def test_text_video_to_text_001(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -114,7 +114,7 @@ def test_text_video_to_text_001(omni_server, openai_client) -> None:
|
|||||||
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_sequential_requests_independent(omni_server, openai_client) -> None:
|
def test_sequential_requests_independent(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -157,7 +157,7 @@ def test_sequential_requests_independent(omni_server, openai_client) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_text_to_audio_long_output_001(omni_server, openai_client) -> None:
|
def test_text_to_audio_long_output_001(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -185,7 +185,7 @@ def test_text_to_audio_long_output_001(omni_server, openai_client) -> None:
|
|||||||
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_text_to_audio_long_form_001(omni_server, openai_client) -> None:
|
def test_text_to_audio_long_form_001(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -208,7 +208,7 @@ def test_text_to_audio_long_form_001(omni_server, openai_client) -> None:
|
|||||||
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"], "npu": "A3"}, num_cards=1)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_chinese_text_to_audio(omni_server, openai_client) -> None:
|
def test_chinese_text_to_audio(omni_server, openai_client) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ if TOKENIZER:
|
|||||||
pytestmark = [pytest.mark.full_model, pytest.mark.omni]
|
pytestmark = [pytest.mark.full_model, pytest.mark.omni]
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=1)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"omni_server",
|
"omni_server",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def get_max_batch_size(size_type="few"):
|
|||||||
return batch_sizes.get(size_type, 5)
|
return batch_sizes.get(size_type, 5)
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params + reduced_token_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params + reduced_token_params, indirect=True)
|
||||||
def test_text_to_text_audio_001(omni_server, online_client) -> None:
|
def test_text_to_text_audio_001(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -156,7 +156,7 @@ def test_text_to_text_audio_001(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_text_video_to_text_001(omni_server, online_client) -> None:
|
def test_text_video_to_text_001(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -180,7 +180,7 @@ def test_text_video_to_text_001(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params + reduced_token_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params + reduced_token_params, indirect=True)
|
||||||
def test_text_audio_to_text_audio_002(omni_server, online_client) -> None:
|
def test_text_audio_to_text_audio_002(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -205,7 +205,7 @@ def test_text_audio_to_text_audio_002(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_large_image_to_text_audio_001(omni_server, online_client) -> None:
|
def test_large_image_to_text_audio_001(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -233,7 +233,7 @@ def test_large_image_to_text_audio_001(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_text_video_to_text_audio_001(omni_server, online_client) -> None:
|
def test_text_video_to_text_audio_001(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -258,7 +258,7 @@ def test_text_video_to_text_audio_001(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config)
|
online_client.send_omni_request(request_config)
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_audio_in_video_001(omni_server, online_client) -> None:
|
def test_audio_in_video_001(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -284,7 +284,7 @@ def test_audio_in_video_001(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config)
|
online_client.send_omni_request(request_config)
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_audio_in_video_002(omni_server, online_client) -> None:
|
def test_audio_in_video_002(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -311,7 +311,7 @@ def test_audio_in_video_002(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
online_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_audio_in_video_default_loader_sampling_regression(omni_server, online_client) -> None:
|
def test_audio_in_video_default_loader_sampling_regression(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -349,7 +349,7 @@ def test_audio_in_video_default_loader_sampling_regression(omni_server, online_c
|
|||||||
online_client.send_omni_request(dict(base_config))
|
online_client.send_omni_request(dict(base_config))
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_one_word_prompt_001(omni_server, online_client) -> None:
|
def test_one_word_prompt_001(omni_server, online_client) -> None:
|
||||||
"""Catastrophic-regression gate on one-word pronunciation.
|
"""Catastrophic-regression gate on one-word pronunciation.
|
||||||
@@ -403,7 +403,7 @@ def test_one_word_prompt_001(omni_server, online_client) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_speaker_001(omni_server, online_client) -> None:
|
def test_speaker_001(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -428,7 +428,7 @@ def test_speaker_001(omni_server, online_client) -> None:
|
|||||||
online_client.send_omni_request(request_config)
|
online_client.send_omni_request(request_config)
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_speaker_002(omni_server, online_client) -> None:
|
def test_speaker_002(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -463,7 +463,7 @@ def test_speaker_002(omni_server, online_client) -> None:
|
|||||||
print(f"Gender assertion failed, retrying {attempt + 2}/{_max_retries}: {e!r}")
|
print(f"Gender assertion failed, retrying {attempt + 2}/{_max_retries}: {e!r}")
|
||||||
|
|
||||||
|
|
||||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
@hardware_test(res={"cuda": ["H100", "B200"], "rocm": "MI325"}, num_cards=2)
|
||||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||||
def test_text_to_audio_long_output_001(omni_server, online_client) -> None:
|
def test_text_to_audio_long_output_001(omni_server, online_client) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ affect in-process Omni (``OmniRunner`` / offline e2e) vs subprocess-based
|
|||||||
``OmniServer`` tests.
|
``OmniServer`` tests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
def skip_if_gated_repo_inaccessible(
|
def skip_if_gated_repo_inaccessible(
|
||||||
@@ -36,11 +36,15 @@ def skip_if_gated_repo_inaccessible(
|
|||||||
try:
|
try:
|
||||||
hf_hub_download(repo_id=repo_id, filename=filename, revision=revision)
|
hf_hub_download(repo_id=repo_id, filename=filename, revision=revision)
|
||||||
except GatedRepoError as exc:
|
except GatedRepoError as exc:
|
||||||
|
import pytest
|
||||||
|
|
||||||
pytest.skip(
|
pytest.skip(
|
||||||
f"Skipping: gated HF repo {repo_id!r} inaccessible to the current "
|
f"Skipping: gated HF repo {repo_id!r} inaccessible to the current "
|
||||||
f"HF_TOKEN ({exc}). See docs/contributing/ci/hf_credentials.md."
|
f"HF_TOKEN ({exc}). See docs/contributing/ci/hf_credentials.md."
|
||||||
)
|
)
|
||||||
except RepositoryNotFoundError as exc:
|
except RepositoryNotFoundError as exc:
|
||||||
|
import pytest
|
||||||
|
|
||||||
pytest.skip(f"Skipping: HF repo {repo_id!r} not found ({exc}).")
|
pytest.skip(f"Skipping: HF repo {repo_id!r} not found ({exc}).")
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
+40
-23
@@ -16,6 +16,11 @@ from typing import TYPE_CHECKING
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
# CUDA (and only CUDA) may list several SKUs so one item matches H100 nightly
|
||||||
|
# and B200 mirror collection: ``res={"cuda": ["H100", "B200"]}``.
|
||||||
|
# Kept under TYPE_CHECKING so Python 3.9 agents do not evaluate ``str | list``.
|
||||||
|
SkuSpec = str | list[str] | tuple[str, ...]
|
||||||
|
|
||||||
# Marker description tags in ``pyproject.toml`` ``tool.pytest.ini_options.markers``.
|
# Marker description tags in ``pyproject.toml`` ``tool.pytest.ini_options.markers``.
|
||||||
# Example: ``"H100: [hardware-resource] [cuda] Tests that require H100 GPU"``.
|
# Example: ``"H100: [hardware-resource] [cuda] Tests that require H100 GPU"``.
|
||||||
_HARDWARE_RESOURCE_MARKER_TAG = "[hardware-resource]"
|
_HARDWARE_RESOURCE_MARKER_TAG = "[hardware-resource]"
|
||||||
@@ -108,18 +113,25 @@ def get_level_markers() -> frozenset[str]:
|
|||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=8)
|
@lru_cache(maxsize=8)
|
||||||
def _skus_for_platform(platform: str) -> frozenset[str]:
|
def get_skus_for_platform(platform: str) -> frozenset[str]:
|
||||||
"""SKU names tagged ``[hardware-resource]`` and ``[platform]`` (e.g. ``[cuda]``)."""
|
"""SKU names tagged ``[hardware-resource]`` and ``[platform]`` (e.g. ``[cuda]``)."""
|
||||||
return get_hardware_mark_list() & _marker_names_with_tag(f"[{platform}]")
|
return get_hardware_mark_list() & _marker_names_with_tag(f"[{platform}]")
|
||||||
|
|
||||||
|
|
||||||
def _require_sku(platform: str, res: str) -> None:
|
def _require_sku(platform: str, res: str) -> None:
|
||||||
allowed = _skus_for_platform(platform)
|
allowed = get_skus_for_platform(platform)
|
||||||
if res not in allowed:
|
if res not in allowed:
|
||||||
supported = ", ".join(sorted(allowed)) or f"(none tagged [hardware-resource] [{platform}])"
|
supported = ", ".join(sorted(allowed)) or f"(none tagged [hardware-resource] [{platform}])"
|
||||||
raise ValueError(f"Invalid {platform} resource type: {res}. Supported: {supported}")
|
raise ValueError(f"Invalid {platform} resource type: {res}. Supported: {supported}")
|
||||||
|
|
||||||
|
|
||||||
|
def _as_sku_list(res: SkuSpec) -> list[str]:
|
||||||
|
skus = [res] if isinstance(res, str) else list(res)
|
||||||
|
if not skus:
|
||||||
|
raise ValueError("resource list must not be empty")
|
||||||
|
return skus
|
||||||
|
|
||||||
|
|
||||||
def _cards_mark(num_cards: int) -> pytest.MarkDecorator:
|
def _cards_mark(num_cards: int) -> pytest.MarkDecorator:
|
||||||
"""Return ``cards_{n}`` so collection can filter with ``-m cards_2`` (etc.).
|
"""Return ``cards_{n}`` so collection can filter with ``-m cards_2`` (etc.).
|
||||||
|
|
||||||
@@ -137,14 +149,15 @@ def _cards_mark(num_cards: int) -> pytest.MarkDecorator:
|
|||||||
return getattr(pytest.mark, f"cards_{num_cards}")
|
return getattr(pytest.mark, f"cards_{num_cards}")
|
||||||
|
|
||||||
|
|
||||||
def _cuda_marks(*, res: str, num_cards: int):
|
def _cuda_marks(*, res: SkuSpec, num_cards: int):
|
||||||
import pytest
|
import pytest
|
||||||
from vllm.platforms import current_platform
|
from vllm.platforms import current_platform
|
||||||
|
|
||||||
_require_sku("cuda", res)
|
skus = _as_sku_list(res)
|
||||||
test_resource = getattr(pytest.mark, res)
|
for sku in skus:
|
||||||
test_platform_detail = pytest.mark.cuda
|
_require_sku("cuda", sku)
|
||||||
marks = [test_resource, test_platform_detail, _cards_mark(num_cards)]
|
marks = [getattr(pytest.mark, sku) for sku in skus]
|
||||||
|
marks.extend([pytest.mark.cuda, _cards_mark(num_cards)])
|
||||||
if num_cards == 1 or not current_platform.is_cuda():
|
if num_cards == 1 or not current_platform.is_cuda():
|
||||||
return marks
|
return marks
|
||||||
return marks + [
|
return marks + [
|
||||||
@@ -204,10 +217,10 @@ def _res_platforms() -> frozenset[str]:
|
|||||||
A ``[hardware-platform]`` name is a ``res`` key only if some SKU is tagged
|
A ``[hardware-platform]`` name is a ``res`` key only if some SKU is tagged
|
||||||
with that platform (``[cuda]``, ``[npu]``, …). ``cpu`` / ``gpu`` have no SKUs.
|
with that platform (``[cuda]``, ``[npu]``, …). ``cpu`` / ``gpu`` have no SKUs.
|
||||||
"""
|
"""
|
||||||
return frozenset(p for p in get_supported_platforms() if _skus_for_platform(p))
|
return frozenset(p for p in get_supported_platforms() if get_skus_for_platform(p))
|
||||||
|
|
||||||
|
|
||||||
def _normalize_num_cards(res: dict[str, str], num_cards: int | dict[str, int]) -> dict[str, int]:
|
def _normalize_num_cards(res: dict[str, SkuSpec], num_cards: int | dict[str, int]) -> dict[str, int]:
|
||||||
allowed = _res_platforms()
|
allowed = _res_platforms()
|
||||||
device_class = get_supported_platforms() - allowed
|
device_class = get_supported_platforms() - allowed
|
||||||
for platform in res:
|
for platform in res:
|
||||||
@@ -232,20 +245,24 @@ def _normalize_num_cards(res: dict[str, str], num_cards: int | dict[str, int]) -
|
|||||||
return num_cards_dict
|
return num_cards_dict
|
||||||
|
|
||||||
|
|
||||||
def _marks_for_platform(platform: str, resource: str, num_cards: int) -> list[pytest.MarkDecorator]:
|
def _marks_for_platform(platform: str, resource: SkuSpec, num_cards: int) -> list[pytest.MarkDecorator]:
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
builders = {
|
if platform == "cuda":
|
||||||
"cuda": _cuda_marks,
|
marks = _cuda_marks(res=resource, num_cards=num_cards)
|
||||||
"rocm": _rocm_marks,
|
else:
|
||||||
"xpu": _xpu_marks,
|
if not isinstance(resource, str):
|
||||||
"musa": _musa_marks,
|
raise ValueError(f"{platform} resource must be a string, got {resource!r}")
|
||||||
"npu": _npu_marks,
|
builders = {
|
||||||
}
|
"rocm": _rocm_marks,
|
||||||
builder = builders.get(platform)
|
"xpu": _xpu_marks,
|
||||||
if builder is None:
|
"musa": _musa_marks,
|
||||||
raise ValueError(f"Unsupported platform: {platform}")
|
"npu": _npu_marks,
|
||||||
marks = builder(res=resource, num_cards=num_cards)
|
}
|
||||||
|
builder = builders.get(platform)
|
||||||
|
if builder is None:
|
||||||
|
raise ValueError(f"Unsupported platform: {platform}")
|
||||||
|
marks = builder(res=resource, num_cards=num_cards)
|
||||||
if platform in _gpu_res_platforms():
|
if platform in _gpu_res_platforms():
|
||||||
return [pytest.mark.gpu] + marks
|
return [pytest.mark.gpu] + marks
|
||||||
return marks
|
return marks
|
||||||
@@ -272,7 +289,7 @@ def _apply_marks(func, marks: list[pytest.MarkDecorator]):
|
|||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
def hardware_marks(*, res: dict[str, str], num_cards: int | dict[str, int] = 1):
|
def hardware_marks(*, res: dict[str, SkuSpec], num_cards: int | dict[str, int] = 1):
|
||||||
"""Return marks for a **single** pytest item.
|
"""Return marks for a **single** pytest item.
|
||||||
|
|
||||||
Different per-platform ``num_cards`` cannot share one item: ``cards_2`` and
|
Different per-platform ``num_cards`` cannot share one item: ``cards_2`` and
|
||||||
@@ -295,7 +312,7 @@ def hardware_marks(*, res: dict[str, str], num_cards: int | dict[str, int] = 1):
|
|||||||
return all_marks
|
return all_marks
|
||||||
|
|
||||||
|
|
||||||
def hardware_test(*, res: dict[str, str], num_cards: int | dict[str, int] = 1):
|
def hardware_test(*, res: dict[str, SkuSpec], num_cards: int | dict[str, int] = 1):
|
||||||
"""Apply hardware marks; split collected items when card counts differ by platform.
|
"""Apply hardware marks; split collected items when card counts differ by platform.
|
||||||
|
|
||||||
A single item cannot carry both ``cards_4`` (CUDA) and ``cards_2`` (ROCm):
|
A single item cannot carry both ``cards_4`` (CUDA) and ``cards_2`` (ROCm):
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import pytest
|
|||||||
|
|
||||||
from tests.helpers.mark import (
|
from tests.helpers.mark import (
|
||||||
_gpu_res_platforms,
|
_gpu_res_platforms,
|
||||||
|
get_skus_for_platform,
|
||||||
|
get_supported_card_counts,
|
||||||
hardware_marks,
|
hardware_marks,
|
||||||
hardware_test,
|
hardware_test,
|
||||||
)
|
)
|
||||||
@@ -21,6 +23,18 @@ def test_gpu_platforms_come_from_pyproject_gpu_tag():
|
|||||||
assert "gpu" not in _gpu_res_platforms()
|
assert "gpu" not in _gpu_res_platforms()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_skus_for_platform_cuda_excludes_other_accelerators():
|
||||||
|
cuda_skus = get_skus_for_platform("cuda")
|
||||||
|
assert cuda_skus >= {"H100", "H200", "H800", "L4", "B200"}
|
||||||
|
assert "MI325" not in cuda_skus
|
||||||
|
assert "A2" not in cuda_skus
|
||||||
|
assert get_skus_for_platform("rocm") == {"MI325"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_supported_card_counts_comes_from_pyproject():
|
||||||
|
assert get_supported_card_counts() == frozenset(range(1, 9))
|
||||||
|
|
||||||
|
|
||||||
def test_default_num_cards_adds_cards_1():
|
def test_default_num_cards_adds_cards_1():
|
||||||
names = _mark_names(hardware_marks(res={"cuda": "H100"}))
|
names = _mark_names(hardware_marks(res={"cuda": "H100"}))
|
||||||
assert "cards_1" in names
|
assert "cards_1" in names
|
||||||
@@ -137,3 +151,14 @@ def test_cuda_registered_skus_attach_sku_and_cards(sku):
|
|||||||
def test_cuda_rejects_sku_from_another_platform():
|
def test_cuda_rejects_sku_from_another_platform():
|
||||||
with pytest.raises(ValueError, match="Invalid cuda resource type"):
|
with pytest.raises(ValueError, match="Invalid cuda resource type"):
|
||||||
hardware_marks(res={"cuda": "MI325"})
|
hardware_marks(res={"cuda": "MI325"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_cuda_multi_sku_attaches_all_skus_and_one_cards_mark():
|
||||||
|
names = _mark_names(hardware_marks(res={"cuda": ["H100", "B200"]}, num_cards=2))
|
||||||
|
assert names >= {"H100", "B200", "cuda", "gpu", "cards_2"}
|
||||||
|
assert "cards_1" not in names
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_cuda_rejects_sku_list():
|
||||||
|
with pytest.raises(ValueError, match="rocm resource must be a string"):
|
||||||
|
hardware_marks(res={"rocm": ["MI325"]})
|
||||||
@@ -2,13 +2,15 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""Unit tests for CosyVoice3 components."""
|
"""Unit tests for CosyVoice3 components."""
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
|
|
||||||
from tests.helpers.mark import hardware_test
|
from vllm_omni.diffusion.config import set_current_diffusion_config
|
||||||
|
from vllm_omni.diffusion.data import AttentionConfig
|
||||||
from vllm_omni.model_executor.models.cosyvoice3.code2wav_core.hifigan import (
|
from vllm_omni.model_executor.models.cosyvoice3.code2wav_core.hifigan import (
|
||||||
CausalHiFTGenerator,
|
CausalHiFTGenerator,
|
||||||
)
|
)
|
||||||
@@ -16,6 +18,17 @@ from vllm_omni.model_executor.models.cosyvoice3.code2wav_core.hifigan import (
|
|||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _force_torch_sdpa():
|
||||||
|
"""Pin TORCH_SDPA so CPU shape tests do not pick CUDA-only backends (FA3)."""
|
||||||
|
od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(default="TORCH_SDPA"),
|
||||||
|
parallel_config=SimpleNamespace(ring_degree=1),
|
||||||
|
)
|
||||||
|
with set_current_diffusion_config(od_config):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def causal_hift():
|
def causal_hift():
|
||||||
return CausalHiFTGenerator(
|
return CausalHiFTGenerator(
|
||||||
@@ -53,8 +66,6 @@ class TestPreLookaheadLayer:
|
|||||||
|
|
||||||
return PreLookaheadLayer(in_channels=512, channels=512, pre_lookahead_len=3)
|
return PreLookaheadLayer(in_channels=512, channels=512, pre_lookahead_len=3)
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@pytest.mark.cpu
|
|
||||||
def test_forward_shape(self, layer):
|
def test_forward_shape(self, layer):
|
||||||
"""Test that output shape matches input shape."""
|
"""Test that output shape matches input shape."""
|
||||||
batch, seq_len, channels = 2, 10, 512
|
batch, seq_len, channels = 2, 10, 512
|
||||||
@@ -64,8 +75,6 @@ class TestPreLookaheadLayer:
|
|||||||
|
|
||||||
assert out.shape == x.shape
|
assert out.shape == x.shape
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@pytest.mark.cpu
|
|
||||||
def test_forward_with_context(self, layer):
|
def test_forward_with_context(self, layer):
|
||||||
"""Test forward with context for streaming."""
|
"""Test forward with context for streaming."""
|
||||||
batch, seq_len, channels = 1, 10, 512
|
batch, seq_len, channels = 1, 10, 512
|
||||||
@@ -77,8 +86,6 @@ class TestPreLookaheadLayer:
|
|||||||
|
|
||||||
assert out.shape == x.shape
|
assert out.shape == x.shape
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@pytest.mark.cpu
|
|
||||||
def test_residual_connection(self, layer):
|
def test_residual_connection(self, layer):
|
||||||
"""Test that residual connection is applied."""
|
"""Test that residual connection is applied."""
|
||||||
batch, seq_len, channels = 1, 5, 512
|
batch, seq_len, channels = 1, 5, 512
|
||||||
@@ -98,10 +105,9 @@ class TestDiTAttention:
|
|||||||
def attention(self):
|
def attention(self):
|
||||||
from vllm_omni.diffusion.models.cosyvoice3_audio.cosyvoice3_dit import DiTAttention
|
from vllm_omni.diffusion.models.cosyvoice3_audio.cosyvoice3_dit import DiTAttention
|
||||||
|
|
||||||
return DiTAttention(dim=512, heads=8, dim_head=64, dropout=0.0)
|
with _force_torch_sdpa():
|
||||||
|
return DiTAttention(dim=512, heads=8, dim_head=64, dropout=0.0)
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_forward_shape(self, attention):
|
def test_forward_shape(self, attention):
|
||||||
"""Test attention output shape."""
|
"""Test attention output shape."""
|
||||||
batch, seq_len, dim = 2, 16, 512
|
batch, seq_len, dim = 2, 16, 512
|
||||||
@@ -111,8 +117,6 @@ class TestDiTAttention:
|
|||||||
|
|
||||||
assert out.shape == x.shape
|
assert out.shape == x.shape
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_forward_with_mask(self, attention):
|
def test_forward_with_mask(self, attention):
|
||||||
"""Test attention with mask."""
|
"""Test attention with mask."""
|
||||||
batch, seq_len, dim = 2, 16, 512
|
batch, seq_len, dim = 2, 16, 512
|
||||||
@@ -126,8 +130,6 @@ class TestDiTAttention:
|
|||||||
# Masked positions should be zero
|
# Masked positions should be zero
|
||||||
assert torch.allclose(out[:, -3:], torch.zeros_like(out[:, -3:]))
|
assert torch.allclose(out[:, -3:], torch.zeros_like(out[:, -3:]))
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_qkv_projections(self, attention):
|
def test_qkv_projections(self, attention):
|
||||||
"""Test that Q/K/V projections exist and have correct dimensions."""
|
"""Test that Q/K/V projections exist and have correct dimensions."""
|
||||||
assert hasattr(attention, "to_q")
|
assert hasattr(attention, "to_q")
|
||||||
@@ -145,10 +147,9 @@ class TestDiTBlock:
|
|||||||
def block(self):
|
def block(self):
|
||||||
from vllm_omni.diffusion.models.cosyvoice3_audio.cosyvoice3_dit import DiTBlock
|
from vllm_omni.diffusion.models.cosyvoice3_audio.cosyvoice3_dit import DiTBlock
|
||||||
|
|
||||||
return DiTBlock(dim=512, heads=8, dim_head=64, ff_mult=4, dropout=0.0)
|
with _force_torch_sdpa():
|
||||||
|
return DiTBlock(dim=512, heads=8, dim_head=64, ff_mult=4, dropout=0.0)
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_forward_shape(self, block):
|
def test_forward_shape(self, block):
|
||||||
"""Test block output shape."""
|
"""Test block output shape."""
|
||||||
batch, seq_len, dim = 2, 16, 512
|
batch, seq_len, dim = 2, 16, 512
|
||||||
@@ -159,8 +160,6 @@ class TestDiTBlock:
|
|||||||
|
|
||||||
assert out.shape == x.shape
|
assert out.shape == x.shape
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_adalayernorm_modulation(self, block):
|
def test_adalayernorm_modulation(self, block):
|
||||||
"""Test that AdaLayerNorm modulates based on timestep."""
|
"""Test that AdaLayerNorm modulates based on timestep."""
|
||||||
batch, seq_len, dim = 1, 8, 512
|
batch, seq_len, dim = 1, 8, 512
|
||||||
@@ -182,21 +181,20 @@ class TestDiT:
|
|||||||
def dit(self):
|
def dit(self):
|
||||||
from vllm_omni.diffusion.models.cosyvoice3_audio.cosyvoice3_dit import DiT
|
from vllm_omni.diffusion.models.cosyvoice3_audio.cosyvoice3_dit import DiT
|
||||||
|
|
||||||
return DiT(
|
with _force_torch_sdpa():
|
||||||
dim=256,
|
return DiT(
|
||||||
depth=2,
|
dim=256,
|
||||||
heads=4,
|
depth=2,
|
||||||
dim_head=64,
|
heads=4,
|
||||||
dropout=0.0,
|
dim_head=64,
|
||||||
ff_mult=2,
|
dropout=0.0,
|
||||||
mel_dim=80,
|
ff_mult=2,
|
||||||
mu_dim=80,
|
mel_dim=80,
|
||||||
spk_dim=80,
|
mu_dim=80,
|
||||||
long_skip_connection=True,
|
spk_dim=80,
|
||||||
)
|
long_skip_connection=True,
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_forward_shape(self, dit):
|
def test_forward_shape(self, dit):
|
||||||
"""Test DiT forward output shape."""
|
"""Test DiT forward output shape."""
|
||||||
batch, mel_dim, seq_len = 1, 80, 32
|
batch, mel_dim, seq_len = 1, 80, 32
|
||||||
@@ -211,8 +209,6 @@ class TestDiT:
|
|||||||
|
|
||||||
assert out.shape == (batch, mel_dim, seq_len)
|
assert out.shape == (batch, mel_dim, seq_len)
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_timestep_embedding(self, dit):
|
def test_timestep_embedding(self, dit):
|
||||||
"""Test that different timesteps produce different outputs."""
|
"""Test that different timesteps produce different outputs."""
|
||||||
batch, mel_dim, seq_len = 1, 80, 16
|
batch, mel_dim, seq_len = 1, 80, 16
|
||||||
@@ -245,8 +241,6 @@ class TestCFM:
|
|||||||
|
|
||||||
return DummyEstimator()
|
return DummyEstimator()
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@pytest.mark.cpu
|
|
||||||
def test_causal_conditional_cfm_forward(self, dummy_estimator):
|
def test_causal_conditional_cfm_forward(self, dummy_estimator):
|
||||||
"""Test CausalConditionalCFM forward pass."""
|
"""Test CausalConditionalCFM forward pass."""
|
||||||
from omegaconf import DictConfig
|
from omegaconf import DictConfig
|
||||||
@@ -432,18 +426,17 @@ class TestCFM:
|
|||||||
class TestSDPAFallback:
|
class TestSDPAFallback:
|
||||||
"""Test SDPA fallback for float32 inputs."""
|
"""Test SDPA fallback for float32 inputs."""
|
||||||
|
|
||||||
@pytest.mark.core_model
|
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
|
||||||
def test_float32_uses_sdpa(self):
|
def test_float32_uses_sdpa(self):
|
||||||
"""Test that float32 inputs use SDPA fallback."""
|
"""Test that float32 inputs use SDPA fallback."""
|
||||||
from vllm_omni.diffusion.attention.layer import Attention
|
from vllm_omni.diffusion.attention.layer import Attention
|
||||||
|
|
||||||
attn = Attention(
|
with _force_torch_sdpa():
|
||||||
num_heads=8,
|
attn = Attention(
|
||||||
head_size=64,
|
num_heads=8,
|
||||||
causal=False,
|
head_size=64,
|
||||||
softmax_scale=1.0 / 8.0,
|
causal=False,
|
||||||
)
|
softmax_scale=1.0 / 8.0,
|
||||||
|
)
|
||||||
|
|
||||||
batch, seq_len, heads, dim = 1, 16, 8, 64
|
batch, seq_len, heads, dim = 1, 16, 8, 64
|
||||||
q = torch.randn(batch, seq_len, heads, dim, dtype=torch.float32)
|
q = torch.randn(batch, seq_len, heads, dim, dtype=torch.float32)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Parse pytest commands from Buildkite .buildkite/cuda/test-{ready,merge,nightly,weekly}.yml;
|
Parse pytest commands from Buildkite .buildkite/cuda/test-{ready,merge,nightly,weekly}.yml;
|
||||||
collect test cases (including parametrized) via pytest --collect-only -q and produce an HTML report.
|
collect test cases (including parametrized) via pytest --collect-only -q and produce an HTML report.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
#
|
#
|
||||||
# Extract steps from .buildkite/cuda/test-nightly.yml that contain pytest, synthesize
|
# Extract steps from .buildkite/cuda/test-nightly.yml that contain pytest, synthesize
|
||||||
# small bash wrappers (exports + pytest), run them, and tee output to logs named
|
# small bash wrappers (exports + pytest), run them, and tee output to logs named
|
||||||
@@ -259,7 +259,7 @@ _nightly_log_dir_basename() {
|
|||||||
|
|
||||||
BUILDKITE_REL=".buildkite/cuda/test-nightly.yml"
|
BUILDKITE_REL=".buildkite/cuda/test-nightly.yml"
|
||||||
|
|
||||||
_find_repo_containing_nightly() {
|
_find_repo_containing_yml() {
|
||||||
local dir="${1:-}"
|
local dir="${1:-}"
|
||||||
[[ -n "$dir" ]] || return 1
|
[[ -n "$dir" ]] || return 1
|
||||||
dir="$(cd "$dir" && pwd)" || return 1
|
dir="$(cd "$dir" && pwd)" || return 1
|
||||||
@@ -307,10 +307,10 @@ else
|
|||||||
REPO_ROOT="$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || true)"
|
REPO_ROOT="$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
if [[ -z "${REPO_ROOT}" ]]; then
|
if [[ -z "${REPO_ROOT}" ]]; then
|
||||||
REPO_ROOT="$(_find_repo_containing_nightly "${PWD}" || true)"
|
REPO_ROOT="$(_find_repo_containing_yml "${PWD}" || true)"
|
||||||
fi
|
fi
|
||||||
if [[ -z "${REPO_ROOT}" ]]; then
|
if [[ -z "${REPO_ROOT}" ]]; then
|
||||||
REPO_ROOT="$(_find_repo_containing_nightly "${SCRIPT_DIR}" || true)"
|
REPO_ROOT="$(_find_repo_containing_yml "${SCRIPT_DIR}" || true)"
|
||||||
fi
|
fi
|
||||||
if [[ -z "${REPO_ROOT}" ]]; then
|
if [[ -z "${REPO_ROOT}" ]]; then
|
||||||
echo "Could not locate ${BUILDKITE_REL}. Set REPO_ROOT or YML, run from inside the vllm-omni clone," >&2
|
echo "Could not locate ${BUILDKITE_REL}. Set REPO_ROOT or YML, run from inside the vllm-omni clone," >&2
|
||||||
@@ -326,7 +326,7 @@ fi
|
|||||||
|
|
||||||
echo "Log directory: ${LOG_DIR}" >&2
|
echo "Log directory: ${LOG_DIR}" >&2
|
||||||
|
|
||||||
# Require nightly YAML only when at least one non-stability test kind needs it (validated in Python too).
|
# Require test-nightly.yml only when at least one non-stability test kind needs it (validated in Python too).
|
||||||
_needs_nightly_yml=0
|
_needs_nightly_yml=0
|
||||||
IFS=',' read -ra _TTOK <<< "${TEST_TYPE}"
|
IFS=',' read -ra _TTOK <<< "${TEST_TYPE}"
|
||||||
for _t in "${_TTOK[@]}"; do
|
for _t in "${_TTOK[@]}"; do
|
||||||
@@ -971,7 +971,6 @@ PERF_RESULTS_SRC="${REPO_ROOT}/tests/dfx/perf/results"
|
|||||||
# Copy perf benchmark JSON written under tests/dfx/perf/results/ during this run into LOG_DIR.
|
# Copy perf benchmark JSON written under tests/dfx/perf/results/ during this run into LOG_DIR.
|
||||||
_collect_perf_result_jsons() {
|
_collect_perf_result_jsons() {
|
||||||
local since_epoch="${1:?}"
|
local since_epoch="${1:?}"
|
||||||
local dest="${LOG_DIR}/perf_results"
|
|
||||||
|
|
||||||
if [[ ! -d "${PERF_RESULTS_SRC}" ]]; then
|
if [[ ! -d "${PERF_RESULTS_SRC}" ]]; then
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ run_shellcheck() {
|
|||||||
local f
|
local f
|
||||||
for f in "$@"; do
|
for f in "$@"; do
|
||||||
if should_lint "$f"; then
|
if should_lint "$f"; then
|
||||||
"$SHELLCHECK_BIN" -s bash "$f"
|
# -x honors `# shellcheck source=` so sourced helpers are followed
|
||||||
|
# (avoids SC1091 / false SC2034 on BUILDKITE_REL in entry scripts).
|
||||||
|
"$SHELLCHECK_BIN" -s bash -x "$f"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -75,5 +77,5 @@ fi
|
|||||||
# Direct invocation with no args: lint every tracked *.sh.
|
# Direct invocation with no args: lint every tracked *.sh.
|
||||||
while IFS= read -r -d '' f || [ -n "$f" ]; do
|
while IFS= read -r -d '' f || [ -n "$f" ]; do
|
||||||
git check-ignore -q "$f" && continue
|
git check-ignore -q "$f" && continue
|
||||||
"$SHELLCHECK_BIN" -s bash "$f"
|
"$SHELLCHECK_BIN" -s bash -x "$f"
|
||||||
done < <(find . -path ./.git -prune -o -name "*.sh" -print0)
|
done < <(find . -path ./.git -prune -o -name "*.sh" -print0)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
#
|
#
|
||||||
# Shared helpers for tools/run_*_jobs.sh (source this file; do not execute).
|
# Shared helpers for tools/run_*_jobs.sh (source this file; do not execute).
|
||||||
#
|
#
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
# metadata.
|
# metadata.
|
||||||
|
|
||||||
if [[ -n "${_RUN_JOBS_COMMON_LOADED:-}" ]]; then
|
if [[ -n "${_RUN_JOBS_COMMON_LOADED:-}" ]]; then
|
||||||
return 0 2>/dev/null || exit 0
|
return 0
|
||||||
fi
|
fi
|
||||||
_RUN_JOBS_COMMON_LOADED=1
|
_RUN_JOBS_COMMON_LOADED=1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
#
|
#
|
||||||
# Extract steps from .buildkite/cuda/test-merge.yml that contain pytest, synthesize
|
# Extract steps from .buildkite/cuda/test-merge.yml that contain pytest, synthesize
|
||||||
# small bash wrappers (exports + pytest), run them, and tee output to logs named
|
# small bash wrappers (exports + pytest), run them, and tee output to logs named
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
#
|
#
|
||||||
# Extract steps from .buildkite/cuda/test-ready.yml that contain pytest, synthesize
|
# Extract steps from .buildkite/cuda/test-ready.yml that contain pytest, synthesize
|
||||||
# small bash wrappers (exports + pytest), run them, and tee output to logs named
|
# small bash wrappers (exports + pytest), run them, and tee output to logs named
|
||||||
|
|||||||
@@ -70,7 +70,15 @@ class AttentionBackend(ABC):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_attention_mask(cls) -> bool:
|
def supports_attention_mask(cls, attention_spec: object | None = None) -> bool:
|
||||||
|
"""Return whether this backend can consume a nontrivial ``attn_mask``.
|
||||||
|
|
||||||
|
``attention_spec`` is the resolved per-role config when the user picked
|
||||||
|
a backend explicitly. Implementations that depend on kernel variant
|
||||||
|
(for example FlashInfer cute-dsl vs fa2) must consult it so capability
|
||||||
|
probes match a runnable configuration.
|
||||||
|
"""
|
||||||
|
del attention_spec
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from torch.nn.attention import SDPBackend, sdpa_kernel
|
from torch.nn.attention import SDPBackend, sdpa_kernel
|
||||||
from vllm.logger import init_logger
|
|
||||||
|
|
||||||
from vllm_omni.diffusion.attention.backends.abstract import (
|
from vllm_omni.diffusion.attention.backends.abstract import (
|
||||||
AttentionBackend,
|
AttentionBackend,
|
||||||
@@ -12,23 +11,35 @@ from vllm_omni.diffusion.attention.backends.abstract import (
|
|||||||
)
|
)
|
||||||
from vllm_omni.diffusion.attention.backends.sdpa import _maybe_reshape_attn_mask
|
from vllm_omni.diffusion.attention.backends.sdpa import _maybe_reshape_attn_mask
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CuDNNAttentionBackend(AttentionBackend):
|
class CuDNNAttentionBackend(AttentionBackend):
|
||||||
accept_output_buffer: bool = True
|
accept_output_buffer: bool = True
|
||||||
supports_prefix_kv_slicing: bool = True
|
supports_prefix_kv_slicing: bool = True
|
||||||
|
|
||||||
|
# cuDNN 9.5+ FMHA on Blackwell: head_dim divisible by 8 and at most 256
|
||||||
|
# for BF16/FP16. Used by automatic platform selection; explicit CUDNN_ATTN
|
||||||
|
# raises when the configured head size is outside this set.
|
||||||
|
_MAX_HEAD_SIZE = 256
|
||||||
|
_HEAD_SIZE_MULTIPLE = 8
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_attention_mask(cls) -> bool:
|
def supports_attention_mask(cls, attention_spec: object | None = None) -> bool:
|
||||||
|
del attention_spec
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_supported_head_sizes() -> list[int]:
|
def get_supported_head_sizes() -> list[int]:
|
||||||
# cuDNN 9.5+ FMHA on Blackwell supports any head_dim divisible by 8
|
return list(
|
||||||
# up to 256 for BF16/FP16. Empty list = "accept any"; the sdpa_kernel
|
range(
|
||||||
# fallback chain handles shapes cuDNN can't take.
|
CuDNNAttentionBackend._HEAD_SIZE_MULTIPLE,
|
||||||
return []
|
CuDNNAttentionBackend._MAX_HEAD_SIZE + 1,
|
||||||
|
CuDNNAttentionBackend._HEAD_SIZE_MULTIPLE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def supports_head_size(cls, head_size: int) -> bool:
|
||||||
|
return 0 < head_size <= cls._MAX_HEAD_SIZE and head_size % cls._HEAD_SIZE_MULTIPLE == 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_name() -> str:
|
def get_name() -> str:
|
||||||
@@ -52,6 +63,9 @@ class CuDNNAttentionImpl(AttentionImpl):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.causal = causal
|
self.causal = causal
|
||||||
self.softmax_scale = softmax_scale
|
self.softmax_scale = softmax_scale
|
||||||
|
# Set when AttentionConfig (or --diffusion-attention-backend) selected
|
||||||
|
# CUDNN_ATTN. The kv_seq_len=1 MATH path is automatic-only.
|
||||||
|
self.backend_explicit = bool(extra_impl_args.get("backend_explicit", False))
|
||||||
|
|
||||||
def forward_cuda(
|
def forward_cuda(
|
||||||
self,
|
self,
|
||||||
@@ -84,54 +98,32 @@ class CuDNNAttentionImpl(AttentionImpl):
|
|||||||
)
|
)
|
||||||
|
|
||||||
enable_gqa = query.shape[2] != key.shape[2]
|
enable_gqa = query.shape[2] != key.shape[2]
|
||||||
|
kv_seq_len = key.shape[1]
|
||||||
query, key, value = (x.permute(0, 2, 1, 3) for x in (query, key, value))
|
query, key, value = (x.permute(0, 2, 1, 3) for x in (query, key, value))
|
||||||
# cuDNN has no kernel for degenerate attention shapes when either
|
|
||||||
# sequence has one token. Route those degenerate shapes before entering the
|
|
||||||
# cuDNN-only context: under torch.compile, FakeTensor evaluation raises
|
|
||||||
# before the eager RuntimeError fallback below can run.
|
|
||||||
if query.shape[-2] == 1 or key.shape[-2] == 1:
|
|
||||||
with sdpa_kernel([SDPBackend.MATH]):
|
|
||||||
output = torch.nn.functional.scaled_dot_product_attention(
|
|
||||||
query,
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
attn_mask=attention_mask,
|
|
||||||
dropout_p=0.0,
|
|
||||||
is_causal=self.causal,
|
|
||||||
scale=self.softmax_scale,
|
|
||||||
enable_gqa=enable_gqa,
|
|
||||||
)
|
|
||||||
return output.permute(0, 2, 1, 3)
|
|
||||||
|
|
||||||
# Pin cuDNN exclusively. A priority list like [CUDNN, FLASH, MATH] hits a
|
# Pin one backend only. A priority list like [CUDNN, FLASH, MATH] hits a
|
||||||
# PyTorch SDPA dispatch quirk: when FLASH rejects a non-None attn_mask,
|
# PyTorch SDPA dispatch quirk: when FLASH rejects a non-None attn_mask,
|
||||||
# cuDNN gets runtime-disabled in the same call and the dispatcher falls
|
# cuDNN gets runtime-disabled in the same call and the dispatcher falls
|
||||||
# through to MATH even though cuDNN alone handles the mask fine
|
# through to MATH even though cuDNN alone handles the mask fine
|
||||||
# (~11 ms vs ~215 ms for MATH on sm_120 HV-1.5 shapes).
|
# (~11 ms vs ~215 ms for MATH on sm_120 HV-1.5 shapes).
|
||||||
|
# Explicit CUDNN_ATTN must not silently substitute MATH/EFFICIENT.
|
||||||
#
|
#
|
||||||
# Fall back to the default SDPA dispatcher if cuDNN rejects the shape,
|
# Automatic-only: cuDNN FMHA rejects KV sequence length 1
|
||||||
# e.g. under torch.compile where Dynamo sees a symbolic head_dim and
|
# ("cudnn SDPA does not support key/value sequence length 1"). Dummy
|
||||||
# cuDNN's kernel selection fails for some symbolic attention shapes.
|
# warmup and some I2V layers (e.g. LTX-2) hit this when CUDNN_ATTN is
|
||||||
# The unpinned dispatcher then picks EFFICIENT/MATH instead of raising.
|
# the platform default. MATH is the only kernel that can run that shape.
|
||||||
try:
|
# An explicit CUDNN_ATTN request raises instead of silently switching.
|
||||||
with sdpa_kernel([SDPBackend.CUDNN_ATTENTION]):
|
if kv_seq_len <= 1:
|
||||||
output = torch.nn.functional.scaled_dot_product_attention(
|
if self.backend_explicit:
|
||||||
query,
|
raise ValueError(
|
||||||
key,
|
"CUDNN_ATTN was explicitly selected but cuDNN FMHA does not "
|
||||||
value,
|
f"support key/value sequence length {kv_seq_len}. "
|
||||||
attn_mask=attention_mask,
|
"Select TORCH_SDPA or another backend for this shape."
|
||||||
dropout_p=0.0,
|
|
||||||
is_causal=self.causal,
|
|
||||||
scale=self.softmax_scale,
|
|
||||||
enable_gqa=enable_gqa,
|
|
||||||
)
|
)
|
||||||
except RuntimeError as e:
|
backends = [SDPBackend.MATH]
|
||||||
if "No available kernel" not in str(e):
|
else:
|
||||||
raise
|
backends = [SDPBackend.CUDNN_ATTENTION]
|
||||||
logger.warning_once(
|
with sdpa_kernel(backends):
|
||||||
"cuDNN SDPA rejected this shape; falling back to default SDPA dispatcher. "
|
|
||||||
"Set DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA for the full dispatcher path on every call."
|
|
||||||
)
|
|
||||||
output = torch.nn.functional.scaled_dot_product_attention(
|
output = torch.nn.functional.scaled_dot_product_attention(
|
||||||
query,
|
query,
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ class FlashAttentionBackend(AttentionBackend):
|
|||||||
return current_omni_platform.is_cuda() or current_omni_platform.is_rocm() or current_omni_platform.is_musa()
|
return current_omni_platform.is_cuda() or current_omni_platform.is_rocm() or current_omni_platform.is_musa()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_attention_mask(cls) -> bool:
|
def supports_attention_mask(cls, attention_spec: object | None = None) -> bool:
|
||||||
|
del attention_spec
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -64,7 +65,7 @@ class FlashAttentionBackend(AttentionBackend):
|
|||||||
return FlashAttentionImpl
|
return FlashAttentionImpl
|
||||||
|
|
||||||
|
|
||||||
class FlashAttentionImpl(AttentionImpl):
|
class FlashAttentionImpl(AttentionImpl[AttentionMetadata]):
|
||||||
# Per-platform FP8 KV quantization support.
|
# Per-platform FP8 KV quantization support.
|
||||||
# To enable FP8 on a new platform, add its OmniPlatformEnum value here
|
# To enable FP8 on a new platform, add its OmniPlatformEnum value here
|
||||||
# and handle kv_cache_dtype in the corresponding forward_{platform}().
|
# and handle kv_cache_dtype in the corresponding forward_{platform}().
|
||||||
@@ -157,6 +158,8 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
flash_attn_varlen_func,
|
flash_attn_varlen_func,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if flash_attn_varlen_func is None:
|
||||||
|
raise ImportError("Masked variable-length attention requires flash_attn_varlen_func")
|
||||||
assert attention_mask.ndim == 2, "attention_mask must be 2D, (batch_size, seq_len)"
|
assert attention_mask.ndim == 2, "attention_mask must be 2D, (batch_size, seq_len)"
|
||||||
batch_size, query_length = query.shape[:2]
|
batch_size, query_length = query.shape[:2]
|
||||||
if not self.is_cross_attn and query_length == key.size(1):
|
if not self.is_cross_attn and query_length == key.size(1):
|
||||||
@@ -250,6 +253,8 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
flash_attn_varlen_func,
|
flash_attn_varlen_func,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if flash_attn_varlen_func is None:
|
||||||
|
raise ImportError("Dense variable-length attention requires flash_attn_varlen_func")
|
||||||
batch_size, q_len = query.size()[:2]
|
batch_size, q_len = query.size()[:2]
|
||||||
k_len = key.size(1)
|
k_len = key.size(1)
|
||||||
cu_seqlens_q = torch.arange(0, (batch_size + 1) * q_len, step=q_len, dtype=torch.int32, device=query.device)
|
cu_seqlens_q = torch.arange(0, (batch_size + 1) * q_len, step=q_len, dtype=torch.int32, device=query.device)
|
||||||
@@ -378,7 +383,7 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
key: torch.Tensor,
|
key: torch.Tensor,
|
||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""CUDA/ROCm/MUSA flash attention implementation."""
|
"""CUDA/ROCm/MUSA flash attention implementation."""
|
||||||
from vllm_omni.diffusion.attention.backends.utils.fa import (
|
from vllm_omni.diffusion.attention.backends.utils.fa import (
|
||||||
@@ -422,7 +427,7 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
full_attn_spans,
|
full_attn_spans,
|
||||||
self.softmax_scale,
|
self.softmax_scale,
|
||||||
attn_func,
|
attn_func,
|
||||||
query_ranges=attn_metadata.query_ranges,
|
query_ranges=None if attn_metadata is None else attn_metadata.query_ranges,
|
||||||
)
|
)
|
||||||
|
|
||||||
packed_keys = ("cu_seqlens_q", "cu_seqlens_k", "max_seqlen_q", "max_seqlen_k")
|
packed_keys = ("cu_seqlens_q", "cu_seqlens_k", "max_seqlen_q", "max_seqlen_k")
|
||||||
@@ -473,7 +478,7 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
key: torch.Tensor,
|
key: torch.Tensor,
|
||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""XPU flash attention implementation."""
|
"""XPU flash attention implementation."""
|
||||||
from vllm_omni.diffusion.attention.backends.utils.fa import (
|
from vllm_omni.diffusion.attention.backends.utils.fa import (
|
||||||
@@ -508,7 +513,7 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
key: torch.Tensor,
|
key: torch.Tensor,
|
||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""NPU attention implementation using mindiesd."""
|
"""NPU attention implementation using mindiesd."""
|
||||||
|
|
||||||
@@ -522,7 +527,7 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
key: torch.Tensor,
|
key: torch.Tensor,
|
||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
from vllm_omni.platforms.npu.quant.kv_quant_npu import fp8_rotate_quant_fa
|
from vllm_omni.platforms.npu.quant.kv_quant_npu import fp8_rotate_quant_fa
|
||||||
|
|
||||||
@@ -542,7 +547,7 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
key: torch.Tensor,
|
key: torch.Tensor,
|
||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
from mindiesd import attention_forward
|
from mindiesd import attention_forward
|
||||||
|
|
||||||
@@ -712,8 +717,12 @@ class FlashAttentionImpl(AttentionImpl):
|
|||||||
key = key[:, :used_k]
|
key = key[:, :used_k]
|
||||||
value = value[:, :used_k]
|
value = value[:, :used_k]
|
||||||
|
|
||||||
input_scale = extra.get("laser_input_scale")
|
input_scale_raw = extra.get("laser_input_scale")
|
||||||
preserve_input_range = isinstance(input_scale, (int, float)) and input_scale > 1
|
if isinstance(input_scale_raw, (int, float)) and input_scale_raw > 1:
|
||||||
|
input_scale = float(input_scale_raw)
|
||||||
|
else:
|
||||||
|
input_scale = 1.0
|
||||||
|
preserve_input_range = input_scale > 1
|
||||||
if preserve_input_range:
|
if preserve_input_range:
|
||||||
query = query / input_scale
|
query = query / input_scale
|
||||||
key = key / input_scale
|
key = key / input_scale
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
from collections.abc import Callable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from vllm.logger import init_logger
|
from vllm.logger import init_logger
|
||||||
@@ -44,6 +46,12 @@ def _get_hub_module(repo_id: str):
|
|||||||
return _hub_modules[repo_id]
|
return _hub_modules[repo_id]
|
||||||
|
|
||||||
|
|
||||||
|
def _require_varlen_func(func: Callable[..., Any] | None) -> Callable[..., Any]:
|
||||||
|
if func is None:
|
||||||
|
raise RuntimeError("flash_attn_varlen_func is not available from the Hub kernel")
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
def _run_varlen_dense(
|
def _run_varlen_dense(
|
||||||
varlen_func,
|
varlen_func,
|
||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
@@ -79,7 +87,8 @@ class FlashAttentionHubBackend(AttentionBackend):
|
|||||||
supports_piecewise_spans: bool = True
|
supports_piecewise_spans: bool = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_attention_mask(cls) -> bool:
|
def supports_attention_mask(cls, attention_spec: object | None = None) -> bool:
|
||||||
|
del attention_spec
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -95,7 +104,7 @@ class FlashAttentionHubBackend(AttentionBackend):
|
|||||||
return FlashAttentionHubImpl
|
return FlashAttentionHubImpl
|
||||||
|
|
||||||
|
|
||||||
class FlashAttentionHubImpl(AttentionImpl):
|
class FlashAttentionHubImpl(AttentionImpl[AttentionMetadata]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
num_heads: int,
|
num_heads: int,
|
||||||
@@ -116,8 +125,8 @@ class FlashAttentionHubImpl(AttentionImpl):
|
|||||||
logger.warning("FlashAttentionHubImpl ignoring backend_kwargs: %s", list(backend_kwargs.keys()))
|
logger.warning("FlashAttentionHubImpl ignoring backend_kwargs: %s", list(backend_kwargs.keys()))
|
||||||
|
|
||||||
hub_module = _get_hub_module("kernels-community/flash-attn2")
|
hub_module = _get_hub_module("kernels-community/flash-attn2")
|
||||||
self.flash_attn_func = getattr(hub_module, "flash_attn_func", None)
|
self.flash_attn_func: Callable[..., Any] | None = getattr(hub_module, "flash_attn_func", None)
|
||||||
self.flash_attn_varlen_func = getattr(hub_module, "flash_attn_varlen_func", None)
|
self.flash_attn_varlen_func: Callable[..., Any] | None = getattr(hub_module, "flash_attn_varlen_func", None)
|
||||||
if self.flash_attn_func is None and self.flash_attn_varlen_func is None:
|
if self.flash_attn_func is None and self.flash_attn_varlen_func is None:
|
||||||
raise RuntimeError("Failed to load flash-attn2 kernel from HuggingFace Hub: no functions found")
|
raise RuntimeError("Failed to load flash-attn2 kernel from HuggingFace Hub: no functions found")
|
||||||
|
|
||||||
@@ -130,7 +139,7 @@ class FlashAttentionHubImpl(AttentionImpl):
|
|||||||
if attn_func is not None:
|
if attn_func is not None:
|
||||||
return self._unwrap_flash_output(attn_func(q, k, v, **kwargs))
|
return self._unwrap_flash_output(attn_func(q, k, v, **kwargs))
|
||||||
return _run_varlen_dense(
|
return _run_varlen_dense(
|
||||||
self.flash_attn_varlen_func,
|
_require_varlen_func(self.flash_attn_varlen_func),
|
||||||
q,
|
q,
|
||||||
k,
|
k,
|
||||||
v,
|
v,
|
||||||
@@ -157,7 +166,7 @@ class FlashAttentionHubImpl(AttentionImpl):
|
|||||||
query, key, value, attention_mask, query_length, _unpad_input
|
query, key, value, attention_mask, query_length, _unpad_input
|
||||||
)
|
)
|
||||||
|
|
||||||
out_unpad = self.flash_attn_varlen_func(
|
out_unpad = _require_varlen_func(self.flash_attn_varlen_func)(
|
||||||
q,
|
q,
|
||||||
k,
|
k,
|
||||||
v,
|
v,
|
||||||
@@ -180,7 +189,7 @@ class FlashAttentionHubImpl(AttentionImpl):
|
|||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
return _run_varlen_dense(
|
return _run_varlen_dense(
|
||||||
self.flash_attn_varlen_func,
|
_require_varlen_func(self.flash_attn_varlen_func),
|
||||||
query,
|
query,
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
@@ -193,7 +202,7 @@ class FlashAttentionHubImpl(AttentionImpl):
|
|||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
key: torch.Tensor,
|
key: torch.Tensor,
|
||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
attention_mask = attn_metadata.attn_mask if attn_metadata is not None else None
|
attention_mask = attn_metadata.attn_mask if attn_metadata is not None else None
|
||||||
full_attn_spans = attn_metadata.full_attn_spans if attn_metadata is not None else None
|
full_attn_spans = attn_metadata.full_attn_spans if attn_metadata is not None else None
|
||||||
@@ -213,7 +222,7 @@ class FlashAttentionHubImpl(AttentionImpl):
|
|||||||
full_attn_spans,
|
full_attn_spans,
|
||||||
self.softmax_scale,
|
self.softmax_scale,
|
||||||
attn_func,
|
attn_func,
|
||||||
query_ranges=attn_metadata.query_ranges,
|
query_ranges=None if attn_metadata is None else attn_metadata.query_ranges,
|
||||||
)
|
)
|
||||||
|
|
||||||
if attention_mask is not None and torch.any(~attention_mask):
|
if attention_mask is not None and torch.any(~attention_mask):
|
||||||
@@ -246,7 +255,8 @@ class FlashAttention3HubBackend(AttentionBackend):
|
|||||||
supports_piecewise_spans: bool = True
|
supports_piecewise_spans: bool = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_attention_mask(cls) -> bool:
|
def supports_attention_mask(cls, attention_spec: object | None = None) -> bool:
|
||||||
|
del attention_spec
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -262,7 +272,7 @@ class FlashAttention3HubBackend(AttentionBackend):
|
|||||||
return FlashAttention3HubImpl
|
return FlashAttention3HubImpl
|
||||||
|
|
||||||
|
|
||||||
class FlashAttention3HubImpl(AttentionImpl):
|
class FlashAttention3HubImpl(AttentionImpl[AttentionMetadata]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
num_heads: int,
|
num_heads: int,
|
||||||
@@ -283,8 +293,8 @@ class FlashAttention3HubImpl(AttentionImpl):
|
|||||||
logger.warning("FlashAttention3HubImpl ignoring backend_kwargs: %s", list(backend_kwargs.keys()))
|
logger.warning("FlashAttention3HubImpl ignoring backend_kwargs: %s", list(backend_kwargs.keys()))
|
||||||
|
|
||||||
hub_module = _get_hub_module("kernels-community/flash-attn3")
|
hub_module = _get_hub_module("kernels-community/flash-attn3")
|
||||||
self.flash_attn_func = getattr(hub_module, "flash_attn_func", None)
|
self.flash_attn_func: Callable[..., Any] | None = getattr(hub_module, "flash_attn_func", None)
|
||||||
self.flash_attn_varlen_func = getattr(hub_module, "flash_attn_varlen_func", None)
|
self.flash_attn_varlen_func: Callable[..., Any] | None = getattr(hub_module, "flash_attn_varlen_func", None)
|
||||||
if self.flash_attn_func is None and self.flash_attn_varlen_func is None:
|
if self.flash_attn_func is None and self.flash_attn_varlen_func is None:
|
||||||
raise RuntimeError("Failed to load flash-attn3 kernel from HuggingFace Hub: no functions found")
|
raise RuntimeError("Failed to load flash-attn3 kernel from HuggingFace Hub: no functions found")
|
||||||
|
|
||||||
@@ -297,7 +307,7 @@ class FlashAttention3HubImpl(AttentionImpl):
|
|||||||
if attn_func is not None:
|
if attn_func is not None:
|
||||||
return self._unwrap_flash_output(attn_func(q, k, v, **kwargs))
|
return self._unwrap_flash_output(attn_func(q, k, v, **kwargs))
|
||||||
return _run_varlen_dense(
|
return _run_varlen_dense(
|
||||||
self.flash_attn_varlen_func,
|
_require_varlen_func(self.flash_attn_varlen_func),
|
||||||
q,
|
q,
|
||||||
k,
|
k,
|
||||||
v,
|
v,
|
||||||
@@ -324,7 +334,7 @@ class FlashAttention3HubImpl(AttentionImpl):
|
|||||||
query, key, value, attention_mask, query_length, _unpad_input
|
query, key, value, attention_mask, query_length, _unpad_input
|
||||||
)
|
)
|
||||||
|
|
||||||
out_unpad = self.flash_attn_varlen_func(
|
out_unpad = _require_varlen_func(self.flash_attn_varlen_func)(
|
||||||
q,
|
q,
|
||||||
k,
|
k,
|
||||||
v,
|
v,
|
||||||
@@ -347,7 +357,7 @@ class FlashAttention3HubImpl(AttentionImpl):
|
|||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
return _run_varlen_dense(
|
return _run_varlen_dense(
|
||||||
self.flash_attn_varlen_func,
|
_require_varlen_func(self.flash_attn_varlen_func),
|
||||||
query,
|
query,
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
@@ -360,7 +370,7 @@ class FlashAttention3HubImpl(AttentionImpl):
|
|||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
key: torch.Tensor,
|
key: torch.Tensor,
|
||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
attention_mask = attn_metadata.attn_mask if attn_metadata is not None else None
|
attention_mask = attn_metadata.attn_mask if attn_metadata is not None else None
|
||||||
full_attn_spans = attn_metadata.full_attn_spans if attn_metadata is not None else None
|
full_attn_spans = attn_metadata.full_attn_spans if attn_metadata is not None else None
|
||||||
@@ -380,7 +390,7 @@ class FlashAttention3HubImpl(AttentionImpl):
|
|||||||
full_attn_spans,
|
full_attn_spans,
|
||||||
self.softmax_scale,
|
self.softmax_scale,
|
||||||
attn_func,
|
attn_func,
|
||||||
query_ranges=attn_metadata.query_ranges,
|
query_ranges=None if attn_metadata is None else attn_metadata.query_ranges,
|
||||||
)
|
)
|
||||||
|
|
||||||
if attention_mask is not None and torch.any(~attention_mask):
|
if attention_mask is not None and torch.any(~attention_mask):
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from packaging.version import InvalidVersion, Version
|
from packaging.version import InvalidVersion, Version
|
||||||
@@ -13,6 +16,9 @@ from vllm_omni.diffusion.attention.backends.abstract import (
|
|||||||
AttentionMetadata,
|
AttentionMetadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -32,9 +38,18 @@ class FlashInferAttentionBackend(AttentionBackend):
|
|||||||
accept_output_buffer: bool = True
|
accept_output_buffer: bool = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_attention_mask(cls) -> bool:
|
def supports_attention_mask(cls, attention_spec: object | None = None) -> bool:
|
||||||
# Non-CuTe batch-prefill backends accept boolean custom masks. CuTe-DSL
|
# fa2/fa3 batch-prefill accept boolean custom masks. cute-dsl does not:
|
||||||
# falls back to SDPA for nontrivial masks.
|
# automatic platform selection may fall back to SDPA, but an explicit
|
||||||
|
# FLASHINFER_ATTN config must not be advertised as mask-capable.
|
||||||
|
requested = "auto"
|
||||||
|
quant = getattr(attention_spec, "quant", None) if attention_spec is not None else None
|
||||||
|
requested_backend = getattr(quant, "flashinfer_backend", None)
|
||||||
|
if requested_backend:
|
||||||
|
requested = requested_backend
|
||||||
|
backend = FlashInferAttentionImpl._select_backend(requested)
|
||||||
|
if backend == "cute-dsl":
|
||||||
|
return attention_spec is None
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -49,7 +64,7 @@ class FlashInferAttentionBackend(AttentionBackend):
|
|||||||
return "FLASHINFER_ATTN"
|
return "FLASHINFER_ATTN"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_impl_cls() -> type["FlashInferAttentionImpl"]:
|
def get_impl_cls() -> type[FlashInferAttentionImpl]:
|
||||||
return FlashInferAttentionImpl
|
return FlashInferAttentionImpl
|
||||||
|
|
||||||
|
|
||||||
@@ -94,13 +109,17 @@ class FlashInferAttentionImpl(AttentionImpl):
|
|||||||
self.dtype_qk = self._check_dtype(quant.get("dtype_qk"), "dtype_qk", self._QK_DTYPES)
|
self.dtype_qk = self._check_dtype(quant.get("dtype_qk"), "dtype_qk", self._QK_DTYPES)
|
||||||
self.dtype_vo = self._check_dtype(quant.get("dtype_vo"), "dtype_vo", self._VO_DTYPES)
|
self.dtype_vo = self._check_dtype(quant.get("dtype_vo"), "dtype_vo", self._VO_DTYPES)
|
||||||
requested_backend = quant.get("flashinfer_backend", "auto")
|
requested_backend = quant.get("flashinfer_backend", "auto")
|
||||||
|
# Set when AttentionConfig (or --diffusion-attention-backend) selected
|
||||||
|
# FLASHINFER_ATTN. Cute-dsl custom-mask SDPA is automatic-only.
|
||||||
|
self.backend_explicit = bool(extra_impl_args.get("backend_explicit", False))
|
||||||
|
self._sdpa_fallback: SDPAImpl | None = None
|
||||||
|
|
||||||
if not HAS_FLASHINFER:
|
if not HAS_FLASHINFER:
|
||||||
raise ImportError("FLASHINFER_ATTN backend requires flashinfer")
|
raise ImportError("FLASHINFER_ATTN backend requires flashinfer")
|
||||||
|
|
||||||
self._check_flashinfer_version()
|
self._check_flashinfer_version()
|
||||||
|
|
||||||
self.flashinfer_backend = self._select_backend(requested_backend, self.device)
|
self.flashinfer_backend = self._select_backend(requested_backend, device=self.device)
|
||||||
workspace_size = 0 if self.flashinfer_backend == "cute-dsl" else 128 * 1024 * 1024
|
workspace_size = 0 if self.flashinfer_backend == "cute-dsl" else 128 * 1024 * 1024
|
||||||
self._workspace = torch.empty(
|
self._workspace = torch.empty(
|
||||||
workspace_size,
|
workspace_size,
|
||||||
@@ -144,10 +163,15 @@ class FlashInferAttentionImpl(AttentionImpl):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _select_backend(requested_backend: str, device: torch.device) -> str:
|
def _select_backend(requested_backend: str, device: torch.device | None = None) -> str:
|
||||||
if requested_backend != "auto":
|
if requested_backend != "auto":
|
||||||
return requested_backend
|
return requested_backend
|
||||||
major, _minor = torch.cuda.get_device_capability(device)
|
try:
|
||||||
|
major, _minor = (
|
||||||
|
torch.cuda.get_device_capability(device) if device is not None else torch.cuda.get_device_capability()
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return "fa2"
|
||||||
if major >= 10:
|
if major >= 10:
|
||||||
return "cute-dsl"
|
return "cute-dsl"
|
||||||
if major >= 9:
|
if major >= 9:
|
||||||
@@ -180,18 +204,14 @@ class FlashInferAttentionImpl(AttentionImpl):
|
|||||||
Returns either ``(qo_len, kv_len)`` (shared across the batch) or
|
Returns either ``(qo_len, kv_len)`` (shared across the batch) or
|
||||||
``(batch_size, qo_len, kv_len)`` (per-sample), or ``None`` when the
|
``(batch_size, qo_len, kv_len)`` (per-sample), or ``None`` when the
|
||||||
mask is all-keep (elide). Only boolean masks are handled here;
|
mask is all-keep (elide). Only boolean masks are handled here;
|
||||||
additive/float masks raise ``ValueError`` so the caller falls back to
|
additive/float masks and shape mismatches raise ``ValueError`` because
|
||||||
SDPA, which applies them with the correct softmax semantics. Shape
|
an explicitly selected backend must not silently switch to SDPA.
|
||||||
mismatches also raise ``ValueError``.
|
|
||||||
"""
|
"""
|
||||||
mask = attn_mask
|
mask = attn_mask
|
||||||
if mask.dtype != torch.bool:
|
if mask.dtype != torch.bool:
|
||||||
# Additive masks (0 / -inf / -1e4 / finfo.min) cannot be faithfully
|
# Additive masks (0 / -inf / -1e4 / finfo.min) cannot be faithfully
|
||||||
# reduced to a boolean keep-mask here; SDPA handles them correctly.
|
# reduced to a boolean keep-mask here; SDPA handles them correctly.
|
||||||
raise ValueError(
|
raise ValueError(f"non-boolean attn_mask (dtype={mask.dtype}); FlashInfer custom_mask is boolean-only")
|
||||||
f"non-boolean attn_mask (dtype={mask.dtype}); FlashInfer custom_mask "
|
|
||||||
"is boolean-only — deferring to SDPA"
|
|
||||||
)
|
|
||||||
# Diffusion masks arrive as (qo,kv), (1,1,kv), (B,1,1,kv), (B,1,qo,kv)
|
# Diffusion masks arrive as (qo,kv), (1,1,kv), (B,1,1,kv), (B,1,qo,kv)
|
||||||
# or (B,H,qo,kv). The mask is identical across heads, so collapse the
|
# or (B,H,qo,kv). The mask is identical across heads, so collapse the
|
||||||
# head dim, but keep a real batch dim — indexing mask[0] would reuse
|
# head dim, but keep a real batch dim — indexing mask[0] would reuse
|
||||||
@@ -216,23 +236,6 @@ class FlashInferAttentionImpl(AttentionImpl):
|
|||||||
# kernel, which reads from GPU memory directly.
|
# kernel, which reads from GPU memory directly.
|
||||||
return mask.contiguous()
|
return mask.contiguous()
|
||||||
|
|
||||||
def _sdpa_fallback(
|
|
||||||
self,
|
|
||||||
query: torch.Tensor,
|
|
||||||
key: torch.Tensor,
|
|
||||||
value: torch.Tensor,
|
|
||||||
attn_metadata: AttentionMetadata | None,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
|
|
||||||
|
|
||||||
impl = SDPAImpl(
|
|
||||||
num_heads=query.shape[2],
|
|
||||||
head_size=query.shape[3],
|
|
||||||
softmax_scale=self.softmax_scale,
|
|
||||||
causal=self.causal,
|
|
||||||
)
|
|
||||||
return impl.forward_cuda(query, key, value, attn_metadata)
|
|
||||||
|
|
||||||
@torch.compiler.disable
|
@torch.compiler.disable
|
||||||
def _plan_wrapper(
|
def _plan_wrapper(
|
||||||
self,
|
self,
|
||||||
@@ -353,6 +356,28 @@ class FlashInferAttentionImpl(AttentionImpl):
|
|||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
return self._wrapper.run(query, key, value)
|
return self._wrapper.run(query, key, value)
|
||||||
|
|
||||||
|
def _sdpa_for_unsupported_mask(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
attn_metadata: AttentionMetadata | None,
|
||||||
|
reason: str,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
|
||||||
|
|
||||||
|
logger.warning_once("FLASHINFER_ATTN falling back to SDPA: %s", reason)
|
||||||
|
fallback = self._sdpa_fallback
|
||||||
|
if fallback is None:
|
||||||
|
fallback = SDPAImpl(
|
||||||
|
num_heads=query.shape[2],
|
||||||
|
head_size=query.shape[3],
|
||||||
|
softmax_scale=self.softmax_scale,
|
||||||
|
causal=self.causal,
|
||||||
|
)
|
||||||
|
self._sdpa_fallback = fallback
|
||||||
|
return fallback.forward_cuda(query, key, value, attn_metadata)
|
||||||
|
|
||||||
def forward_cuda(
|
def forward_cuda(
|
||||||
self,
|
self,
|
||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
@@ -360,6 +385,15 @@ class FlashInferAttentionImpl(AttentionImpl):
|
|||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata | None = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
|
if not HAS_FLASHINFER:
|
||||||
|
raise ImportError(
|
||||||
|
"FLASHINFER_ATTN backend requires flashinfer. "
|
||||||
|
"Install it or set DIFFUSION_ATTENTION_BACKEND to another backend."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Explicit FLASHINFER_ATTN must not silently switch away from the
|
||||||
|
# requested kernel. Automatic platform selection (Blackwell cute-dsl)
|
||||||
|
# may use SDPA for masks that this FlashInfer variant cannot run.
|
||||||
batch_size = query.shape[0]
|
batch_size = query.shape[0]
|
||||||
custom_mask = None
|
custom_mask = None
|
||||||
if attn_metadata is not None and attn_metadata.attn_mask is not None:
|
if attn_metadata is not None and attn_metadata.attn_mask is not None:
|
||||||
@@ -370,14 +404,44 @@ class FlashInferAttentionImpl(AttentionImpl):
|
|||||||
qo_len=query.shape[1],
|
qo_len=query.shape[1],
|
||||||
kv_len=key.shape[1],
|
kv_len=key.shape[1],
|
||||||
)
|
)
|
||||||
except ValueError as error:
|
except ValueError:
|
||||||
logger.debug("Falling back to SDPA for mask path: %s", error)
|
if self.backend_explicit:
|
||||||
return self._sdpa_fallback(query, key, value, attn_metadata)
|
raise
|
||||||
|
return self._sdpa_for_unsupported_mask(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_metadata,
|
||||||
|
"attn_mask is not a FlashInfer boolean custom_mask",
|
||||||
|
)
|
||||||
|
# FlashInfer cannot combine causal masking with a custom_mask; rather
|
||||||
|
# than silently dropping the explicit mask (diverging from SDPA),
|
||||||
|
# require an explicit caller to select a compatible backend.
|
||||||
if custom_mask is not None and self.causal:
|
if custom_mask is not None and self.causal:
|
||||||
return self._sdpa_fallback(query, key, value, attn_metadata)
|
if self.backend_explicit:
|
||||||
|
raise ValueError(
|
||||||
|
"FLASHINFER_ATTN does not support causal=True together with an explicit custom mask."
|
||||||
|
)
|
||||||
|
return self._sdpa_for_unsupported_mask(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_metadata,
|
||||||
|
"causal=True with a custom mask",
|
||||||
|
)
|
||||||
|
|
||||||
if custom_mask is not None and self.flashinfer_backend == "cute-dsl":
|
if custom_mask is not None and self.flashinfer_backend == "cute-dsl":
|
||||||
logger.debug("CuTe DSL does not support custom masks; deferring to SDPA")
|
if self.backend_explicit:
|
||||||
return self._sdpa_fallback(query, key, value, attn_metadata)
|
raise ValueError(
|
||||||
|
"FLASHINFER_ATTN cute-dsl backend does not support custom masks. "
|
||||||
|
"Select TORCH_SDPA or a FlashInfer backend that accepts custom_mask."
|
||||||
|
)
|
||||||
|
return self._sdpa_for_unsupported_mask(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_metadata,
|
||||||
|
"cute-dsl does not support custom masks",
|
||||||
|
)
|
||||||
|
|
||||||
return self._run_batch_prefill(query, key, value, custom_mask)
|
return self._run_batch_prefill(query, key, value, custom_mask)
|
||||||
@@ -22,6 +22,11 @@ except (ImportError, ModuleNotFoundError):
|
|||||||
HAS_FA3 = False
|
HAS_FA3 = False
|
||||||
fa3_fwd_func = None # Low-level forward function (_flash_attn_forward)
|
fa3_fwd_func = None # Low-level forward function (_flash_attn_forward)
|
||||||
fa3_attn_func = None # High-level attention function (flash_attn_func)
|
fa3_attn_func = None # High-level attention function (flash_attn_func)
|
||||||
|
# ``None`` means that the extension does not publish an architecture contract,
|
||||||
|
# so retain the historical import-only behavior. The pinned fa3-fwd wheel is
|
||||||
|
# handled below because importing its Python module succeeds on Blackwell even
|
||||||
|
# though its CUDA binary only contains SM8x/SM90 kernels.
|
||||||
|
FA3_SUPPORTED_CUDA_MAJORS: frozenset[int] | None = None
|
||||||
|
|
||||||
# FA4 detection. The CuTe API returns LSE directly, so it can participate in
|
# FA4 detection. The CuTe API returns LSE directly, so it can participate in
|
||||||
# Ring Attention's numerically stable block-wise output accumulation.
|
# Ring Attention's numerically stable block-wise output accumulation.
|
||||||
@@ -42,16 +47,36 @@ try:
|
|||||||
from flash_attn_interface import flash_attn_func as fa3_attn_func # noqa: F401
|
from flash_attn_interface import flash_attn_func as fa3_attn_func # noqa: F401
|
||||||
|
|
||||||
HAS_FA3 = True
|
HAS_FA3 = True
|
||||||
|
# The source-build FA3 interface is the Hopper implementation. Importing
|
||||||
|
# its Python module on Blackwell does not imply that an SM10x kernel exists.
|
||||||
|
FA3_SUPPORTED_CUDA_MAJORS = frozenset({9})
|
||||||
except (ImportError, ModuleNotFoundError):
|
except (ImportError, ModuleNotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: try fa3_fwd_interface (PyPI package, supports Ampere/Ada/Hopper)
|
# Secondary FA3 import path: fa3_fwd_interface PyPI package.
|
||||||
if not HAS_FA3:
|
if not HAS_FA3:
|
||||||
try:
|
try:
|
||||||
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
|
||||||
|
import fa3_fwd_interface
|
||||||
from fa3_fwd_interface import _flash_attn_forward as fa3_fwd_func # noqa: F401
|
from fa3_fwd_interface import _flash_attn_forward as fa3_fwd_func # noqa: F401
|
||||||
from fa3_fwd_interface import flash_attn_func as fa3_attn_func # noqa: F401
|
from fa3_fwd_interface import flash_attn_func as fa3_attn_func # noqa: F401
|
||||||
|
|
||||||
HAS_FA3 = True
|
HAS_FA3 = True
|
||||||
|
published_majors = getattr(fa3_fwd_interface, "SUPPORTED_CUDA_MAJORS", None)
|
||||||
|
if published_majors is not None:
|
||||||
|
FA3_SUPPORTED_CUDA_MAJORS = frozenset(int(major) for major in published_majors)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
fa3_fwd_version = version("fa3_fwd")
|
||||||
|
except PackageNotFoundError:
|
||||||
|
fa3_fwd_version = None
|
||||||
|
# requirements/cuda.txt pins this wheel. Its fat binary contains
|
||||||
|
# SM80 and SM90 cubins, but no Blackwell kernel. Do not apply this
|
||||||
|
# restriction to an unknown/future build unless it publishes its
|
||||||
|
# own supported-major metadata above.
|
||||||
|
if fa3_fwd_version == "0.0.3":
|
||||||
|
FA3_SUPPORTED_CUDA_MAJORS = frozenset({8, 9})
|
||||||
except (ImportError, ModuleNotFoundError):
|
except (ImportError, ModuleNotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -68,9 +93,8 @@ try:
|
|||||||
HAS_FLASHINFER = True
|
HAS_FLASHINFER = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# flashinfer may raise RuntimeError at import-time for version/binary mismatches.
|
# flashinfer may raise RuntimeError at import-time for version/binary mismatches.
|
||||||
# Treat it as unavailable so the runtime can gracefully fall back to other backends.
|
|
||||||
HAS_FLASHINFER = False
|
HAS_FLASHINFER = False
|
||||||
logger.warning("FlashInfer is unavailable; falling back to other attention backends. Reason: %s", e)
|
logger.warning("FlashInfer ring kernels are unavailable. Reason: %s", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiter # noqa: F401
|
import aiter # noqa: F401
|
||||||
|
|||||||
@@ -59,10 +59,8 @@ def pytorch_attn_forward(
|
|||||||
k shape (bs, seqlen, nhead, hs)
|
k shape (bs, seqlen, nhead, hs)
|
||||||
v shape (bs, seqlen, nhead, hs)
|
v shape (bs, seqlen, nhead, hs)
|
||||||
"""
|
"""
|
||||||
# Fallback logic: Flash Attention does not support float32.
|
|
||||||
# If op_type is 'flash' but dtype is float32, force 'efficient'.
|
|
||||||
if op_type == "flash" and q.dtype == torch.float32:
|
if op_type == "flash" and q.dtype == torch.float32:
|
||||||
op_type = "efficient"
|
raise ValueError("The explicitly requested Flash ring kernel does not support float32.")
|
||||||
|
|
||||||
# Volta (V100, sm70) does not have native bf16 support for the efficient
|
# Volta (V100, sm70) does not have native bf16 support for the efficient
|
||||||
# SDPA kernel. When running Ring attention in bf16 on such GPUs, the
|
# SDPA kernel. When running Ring attention in bf16 on such GPUs, the
|
||||||
@@ -123,6 +121,7 @@ def pytorch_attn_forward(
|
|||||||
# Keep LSE in fp32 for numerical stability when accumulating across ring
|
# Keep LSE in fp32 for numerical stability when accumulating across ring
|
||||||
# steps (update_out_and_lse uses sigmoid/logsigmoid on LSE diffs).
|
# steps (update_out_and_lse uses sigmoid/logsigmoid on LSE diffs).
|
||||||
# Casting LSE down to fp16/bf16 can introduce NaNs on some GPUs/shapes.
|
# Casting LSE down to fp16/bf16 can introduce NaNs on some GPUs/shapes.
|
||||||
|
# PyTorch SDPA returns LSE as (B, H, S).
|
||||||
lse = lse.to(torch.float32)
|
lse = lse.to(torch.float32)
|
||||||
|
|
||||||
if out.dtype != orig_dtype:
|
if out.dtype != orig_dtype:
|
||||||
@@ -173,6 +172,7 @@ def flash_attn_forward(
|
|||||||
alibi_slopes=alibi_slopes,
|
alibi_slopes=alibi_slopes,
|
||||||
return_softmax=return_softmax,
|
return_softmax=return_softmax,
|
||||||
)
|
)
|
||||||
|
# FA2 softmax_lse is (B, H, S), optionally padded on S.
|
||||||
return block_out, block_lse
|
return block_out, block_lse
|
||||||
|
|
||||||
|
|
||||||
@@ -198,6 +198,7 @@ def fa3_forward(q, k, v, dropout_p, softmax_scale, causal, window_size, softcap,
|
|||||||
softcap=softcap if softcap else 0.0,
|
softcap=softcap if softcap else 0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# FA3 softmax_lse is (B, H, S).
|
||||||
return out, softmax_lse
|
return out, softmax_lse
|
||||||
|
|
||||||
|
|
||||||
@@ -236,6 +237,7 @@ def flash_attn4_func_forward(
|
|||||||
softcap=softcap or 0.0,
|
softcap=softcap or 0.0,
|
||||||
return_lse=True,
|
return_lse=True,
|
||||||
)
|
)
|
||||||
|
# FA4 CuTe softmax_lse is (B, H, S).
|
||||||
return out, softmax_lse
|
return out, softmax_lse
|
||||||
|
|
||||||
|
|
||||||
@@ -264,6 +266,7 @@ def flash_attn_forward_aiter(
|
|||||||
return_lse=True,
|
return_lse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Aiter softmax_lse is (B, H, S), matching FA2.
|
||||||
return block_out, block_lse
|
return block_out, block_lse
|
||||||
|
|
||||||
|
|
||||||
@@ -293,7 +296,7 @@ def flashinfer_attn_forward(
|
|||||||
window_left=window_size[0],
|
window_left=window_size[0],
|
||||||
return_lse=True,
|
return_lse=True,
|
||||||
)
|
)
|
||||||
lse = lse.transpose(0, 1)
|
# FlashInfer unbatched LSE is (H, S). Canonical ring layout is (B, H, S).
|
||||||
out, lse = out.unsqueeze(0), lse.unsqueeze(0)
|
out, lse = out.unsqueeze(0), lse.unsqueeze(0)
|
||||||
elif q.ndim == 3:
|
elif q.ndim == 3:
|
||||||
out, lse = single_prefill_with_kv_cache(
|
out, lse = single_prefill_with_kv_cache(
|
||||||
@@ -306,7 +309,7 @@ def flashinfer_attn_forward(
|
|||||||
window_left=window_size[0],
|
window_left=window_size[0],
|
||||||
return_lse=True,
|
return_lse=True,
|
||||||
)
|
)
|
||||||
lse = lse.transpose(0, 1)
|
# Unbatched (H, S) matches canonical heads-major layout without a batch dim.
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid input shape: {q.shape}")
|
raise ValueError(f"Invalid input shape: {q.shape}")
|
||||||
lse = lse / _LOG2_E
|
lse = lse / _LOG2_E
|
||||||
|
|||||||
@@ -2,91 +2,77 @@
|
|||||||
# Copyright (c) 2024, Jiarui Fang.
|
# Copyright (c) 2024, Jiarui Fang.
|
||||||
# Adapted from https://github.com/feifeibear/long-context-attention
|
# Adapted from https://github.com/feifeibear/long-context-attention
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
from vllm.logger import init_logger
|
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
LseLayout = Literal["bhs", "bsh"]
|
||||||
|
|
||||||
__all__ = ["update_out_and_lse", "flatten_varlen_lse", "unflatten_varlen_lse"]
|
__all__ = ["update_out_and_lse", "flatten_varlen_lse", "unflatten_varlen_lse"]
|
||||||
|
|
||||||
|
|
||||||
# Remove torch.jit.script for debugging and flexible shape handling
|
def _normalize_lse(block_lse: torch.Tensor, block_out: torch.Tensor, lse_layout: LseLayout) -> torch.Tensor:
|
||||||
|
"""Normalize a ring kernel LSE to ``(B, S, H, 1)``.
|
||||||
|
|
||||||
|
``lse_layout`` is required. When ``seq_len == num_heads``, ``(B, H, S)`` and
|
||||||
|
``(B, S, H)`` are the same shape, so guessing from dimension sizes would
|
||||||
|
silently transpose a non-symmetric LSE.
|
||||||
|
"""
|
||||||
|
if block_out.ndim != 4:
|
||||||
|
raise ValueError(f"Ring attention output must be 4D (B, S, H, D), got {tuple(block_out.shape)}")
|
||||||
|
|
||||||
|
batch, seq_len, num_heads, _ = block_out.shape
|
||||||
|
if block_lse.shape[0] != batch:
|
||||||
|
raise ValueError(f"Ring LSE batch dimension {block_lse.shape[0]} does not match output batch {batch}.")
|
||||||
|
|
||||||
|
if block_lse.ndim == 4:
|
||||||
|
if block_lse.shape[-1] != 1:
|
||||||
|
raise ValueError(f"4D Ring LSE must end in a singleton dimension, got {tuple(block_lse.shape)}")
|
||||||
|
block_lse = block_lse.squeeze(-1)
|
||||||
|
elif block_lse.ndim != 3:
|
||||||
|
raise ValueError(f"Ring LSE must be 3D or 4D, got {tuple(block_lse.shape)}")
|
||||||
|
|
||||||
|
if lse_layout == "bhs":
|
||||||
|
if block_lse.shape[1] != num_heads or block_lse.shape[2] < seq_len:
|
||||||
|
raise ValueError(
|
||||||
|
f"Ring LSE shape {tuple(block_lse.shape)} is incompatible with output "
|
||||||
|
f"shape {tuple(block_out.shape)}; expected (B, H, S) for lse_layout='bhs'."
|
||||||
|
)
|
||||||
|
return block_lse[:, :, :seq_len].transpose(1, 2).unsqueeze(-1)
|
||||||
|
if lse_layout == "bsh":
|
||||||
|
if block_lse.shape[1] < seq_len or block_lse.shape[2] != num_heads:
|
||||||
|
raise ValueError(
|
||||||
|
f"Ring LSE shape {tuple(block_lse.shape)} is incompatible with output "
|
||||||
|
f"shape {tuple(block_out.shape)}; expected (B, S, H) for lse_layout='bsh'."
|
||||||
|
)
|
||||||
|
return block_lse[:, :seq_len, :].unsqueeze(-1)
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown Ring LSE layout {lse_layout!r}; expected 'bhs' or 'bsh'.")
|
||||||
|
|
||||||
|
|
||||||
def _update_out_and_lse(
|
def _update_out_and_lse(
|
||||||
out: torch.Tensor,
|
out: torch.Tensor,
|
||||||
lse: torch.Tensor,
|
lse: torch.Tensor,
|
||||||
block_out: torch.Tensor,
|
block_out: torch.Tensor,
|
||||||
block_lse: torch.Tensor,
|
block_lse: torch.Tensor,
|
||||||
|
lse_layout: LseLayout,
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
block_out = block_out.to(torch.float32)
|
block_out = block_out.to(torch.float32)
|
||||||
|
if out.shape != block_out.shape:
|
||||||
B, S, H, D = out.shape
|
raise ValueError(
|
||||||
|
f"Ring attention block output shape {tuple(block_out.shape)} does not match "
|
||||||
# --- Shape Correction Logic for block_lse ---
|
f"accumulated output shape {tuple(out.shape)}."
|
||||||
# Goal: block_lse should be (B, S, H, 1) to match out (B, S, H, D)
|
)
|
||||||
|
block_lse = _normalize_lse(block_lse, block_out, lse_layout)
|
||||||
# Debug info
|
|
||||||
# print(f"DEBUG _update: out={out.shape}, block_lse={block_lse.shape}")
|
|
||||||
|
|
||||||
# Case 0: If block_lse is already 4D, check if it matches
|
|
||||||
if block_lse.dim() == 4:
|
|
||||||
if block_lse.shape[1] == S and block_lse.shape[2] == H:
|
|
||||||
pass # Good
|
|
||||||
elif block_lse.shape[1] == H and block_lse.shape[2] == S:
|
|
||||||
block_lse = block_lse.transpose(1, 2)
|
|
||||||
elif block_lse.shape[1] == H and block_lse.shape[2] >= S: # Padding case
|
|
||||||
block_lse = block_lse[:, :, :S, :].transpose(1, 2)
|
|
||||||
# If shape is (B, H, S, 1) but expected (B, S, H, 1) because out is (B, S, H, D)
|
|
||||||
elif block_lse.shape[1] == H and block_lse.shape[2] == S and block_lse.shape[3] == 1:
|
|
||||||
block_lse = block_lse.transpose(1, 2)
|
|
||||||
|
|
||||||
# Case 1: block_lse is 3D (B, H, S) or (B, S, H) or (B, ?, ?)
|
|
||||||
elif block_lse.dim() == 3:
|
|
||||||
# Check for (B, H, S) - Standard SDPA/FA output
|
|
||||||
if block_lse.shape[1] == H and block_lse.shape[2] == S:
|
|
||||||
block_lse = block_lse.transpose(1, 2).unsqueeze(-1)
|
|
||||||
|
|
||||||
# Check for (B, S, H)
|
|
||||||
elif block_lse.shape[1] == S and block_lse.shape[2] == H:
|
|
||||||
block_lse = block_lse.unsqueeze(-1)
|
|
||||||
|
|
||||||
# Check for Padding: (B, H, S_pad) where S_pad >= S
|
|
||||||
elif block_lse.shape[1] == H and block_lse.shape[2] >= S:
|
|
||||||
# print(f"DEBUG: Trimming padding from lse. {block_lse.shape} -> S={S}")
|
|
||||||
block_lse = block_lse[:, :, :S].transpose(1, 2).unsqueeze(-1)
|
|
||||||
|
|
||||||
# Check for weird case: (B, S, H_pad) ? Unlikely for LSE but possible
|
|
||||||
elif block_lse.shape[1] == S and block_lse.shape[2] >= H:
|
|
||||||
block_lse = block_lse[:, :, :H].unsqueeze(-1)
|
|
||||||
|
|
||||||
# Check for flipped weird case: (B, S_pad, H)
|
|
||||||
elif block_lse.shape[1] >= S and block_lse.shape[2] == H:
|
|
||||||
block_lse = block_lse[:, :S, :].unsqueeze(-1)
|
|
||||||
|
|
||||||
# --- Shape Correction for lse (internal state) ---
|
|
||||||
# Ensure lse matches block_lse's corrected shape (B, S, H, 1)
|
|
||||||
if lse.shape != block_lse.shape:
|
if lse.shape != block_lse.shape:
|
||||||
# If lse was initialized with wrong shape, try to fix it
|
raise ValueError(
|
||||||
if lse.dim() == 4 and lse.shape[1] == block_lse.shape[2] and lse.shape[2] == block_lse.shape[1]:
|
f"Ring attention block LSE shape {tuple(block_lse.shape)} does not match "
|
||||||
lse = lse.transpose(1, 2)
|
f"accumulated LSE shape {tuple(lse.shape)}."
|
||||||
elif lse.shape[1] >= S: # slice if lse was initialized with padding
|
)
|
||||||
lse = lse[:, :S, :, :]
|
|
||||||
|
|
||||||
# Final check
|
out = out - F.sigmoid(block_lse - lse) * (out - block_out)
|
||||||
if lse.shape != block_lse.shape:
|
lse = lse - F.logsigmoid(lse - block_lse)
|
||||||
# Force broadcast if possible?
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
out = out - F.sigmoid(block_lse - lse) * (out - block_out)
|
|
||||||
lse = lse - F.logsigmoid(lse - block_lse)
|
|
||||||
except RuntimeError as e:
|
|
||||||
logger.error("_update_out_and_lse failed: %s", e)
|
|
||||||
logger.error("out: %s, lse: %s", out.shape, lse.shape)
|
|
||||||
logger.error("block_out: %s, block_lse: %s", block_out.shape, block_lse.shape)
|
|
||||||
# raise e
|
|
||||||
raise e
|
|
||||||
|
|
||||||
return out, lse
|
return out, lse
|
||||||
|
|
||||||
@@ -97,64 +83,22 @@ def update_out_and_lse(
|
|||||||
block_out: torch.Tensor,
|
block_out: torch.Tensor,
|
||||||
block_lse: torch.Tensor,
|
block_lse: torch.Tensor,
|
||||||
slice_=None,
|
slice_=None,
|
||||||
|
*,
|
||||||
|
lse_layout: LseLayout,
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
if out is None:
|
if out is None:
|
||||||
if slice_ is not None:
|
if slice_ is not None:
|
||||||
raise RuntimeError("first update_out_and_lse should not pass slice_ args")
|
raise RuntimeError("first update_out_and_lse should not pass slice_ args")
|
||||||
|
|
||||||
out = block_out.to(torch.float32)
|
out = block_out.to(torch.float32)
|
||||||
|
lse = _normalize_lse(block_lse, out, lse_layout)
|
||||||
# Initialize LSE with robust logic (same as _update)
|
|
||||||
B, D1, D2, D3 = out.shape
|
|
||||||
|
|
||||||
S_guess = D1
|
|
||||||
H_guess = D2
|
|
||||||
|
|
||||||
if block_lse.dim() == 3:
|
|
||||||
if block_lse.shape[1] == H_guess and block_lse.shape[2] == S_guess:
|
|
||||||
lse = block_lse.transpose(1, 2).unsqueeze(-1)
|
|
||||||
elif block_lse.shape[1] == S_guess and block_lse.shape[2] == H_guess:
|
|
||||||
lse = block_lse.unsqueeze(-1)
|
|
||||||
elif block_lse.shape[1] == H_guess and block_lse.shape[2] >= S_guess: # Padding
|
|
||||||
lse = block_lse[:, :, :S_guess].transpose(1, 2).unsqueeze(-1)
|
|
||||||
elif block_lse.shape[1] == S_guess and block_lse.shape[2] >= H_guess: # Padding/Weird
|
|
||||||
lse = block_lse[:, :, :H_guess].unsqueeze(-1)
|
|
||||||
elif block_lse.shape[1] >= S_guess and block_lse.shape[2] == H_guess:
|
|
||||||
lse = block_lse[:, :S_guess, :].unsqueeze(-1)
|
|
||||||
|
|
||||||
# Reverse case: What if out is (B, H, S, D) so S=D2, H=D1?
|
|
||||||
elif block_lse.shape[1] == D1 and block_lse.shape[2] >= D2: # Matches (H, S)
|
|
||||||
# Then out is (B, H, S, D). We should transpose out!
|
|
||||||
out = out.transpose(1, 2)
|
|
||||||
lse = block_lse[:, :, :D2].transpose(1, 2).unsqueeze(-1) # (B, S, H, 1)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Fallback
|
|
||||||
lse = block_lse.transpose(-2, -1).unsqueeze(dim=-1)
|
|
||||||
else:
|
|
||||||
# Case 0: If block_lse is already 4D, check if it matches
|
|
||||||
if block_lse.dim() == 4:
|
|
||||||
if block_lse.shape[1] == S_guess and block_lse.shape[2] == H_guess:
|
|
||||||
lse = block_lse
|
|
||||||
elif block_lse.shape[1] == H_guess and block_lse.shape[2] == S_guess:
|
|
||||||
lse = block_lse.transpose(1, 2)
|
|
||||||
elif block_lse.shape[1] == H_guess and block_lse.shape[2] >= S_guess: # Padding case
|
|
||||||
lse = block_lse[:, :, :S_guess, :].transpose(1, 2)
|
|
||||||
elif block_lse.shape[1] == D1 and block_lse.shape[2] >= D2: # Matches (H, S)
|
|
||||||
# Then out is (B, H, S, D). We should transpose out!
|
|
||||||
out = out.transpose(1, 2)
|
|
||||||
lse = block_lse[:, :, :D2].transpose(1, 2) # (B, S, H, 1)
|
|
||||||
else:
|
|
||||||
lse = block_lse
|
|
||||||
else:
|
|
||||||
lse = block_lse
|
|
||||||
|
|
||||||
elif slice_ is not None:
|
elif slice_ is not None:
|
||||||
slice_out, slice_lse = out[slice_], lse[slice_]
|
slice_out, slice_lse = out[slice_], lse[slice_]
|
||||||
slice_out, slice_lse = _update_out_and_lse(slice_out, slice_lse, block_out, block_lse)
|
slice_out, slice_lse = _update_out_and_lse(slice_out, slice_lse, block_out, block_lse, lse_layout)
|
||||||
out[slice_], lse[slice_] = slice_out, slice_lse
|
out[slice_], lse[slice_] = slice_out, slice_lse
|
||||||
else:
|
else:
|
||||||
out, lse = _update_out_and_lse(out, lse, block_out, block_lse)
|
out, lse = _update_out_and_lse(out, lse, block_out, block_lse, lse_layout)
|
||||||
return out, lse
|
return out, lse
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ def ring_flash_attn_forward(
|
|||||||
if attn_type == AttnType.SPARSE_SAGE:
|
if attn_type == AttnType.SPARSE_SAGE:
|
||||||
out, lse = block_out, block_lse
|
out, lse = block_out, block_lse
|
||||||
else:
|
else:
|
||||||
out, lse = update_out_and_lse(out, lse, block_out, block_lse)
|
# Ring kernel wrappers canonicalize LSE to (B, H, S).
|
||||||
|
out, lse = update_out_and_lse(out, lse, block_out, block_lse, lse_layout="bhs")
|
||||||
|
|
||||||
if step + 1 != comm.world_size:
|
if step + 1 != comm.world_size:
|
||||||
comm.wait()
|
comm.wait()
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class RingAttentionFunc(torch.autograd.Function):
|
|||||||
causal=is_causal and step == 0,
|
causal=is_causal and step == 0,
|
||||||
op_type=op_type,
|
op_type=op_type,
|
||||||
)
|
)
|
||||||
out, lse = update_out_and_lse(out, lse, block_out, block_lse)
|
out, lse = update_out_and_lse(out, lse, block_out, block_lse, lse_layout="bhs")
|
||||||
|
|
||||||
if step + 1 != comm.world_size:
|
if step + 1 != comm.world_size:
|
||||||
comm.wait()
|
comm.wait()
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ else:
|
|||||||
"SageAttentionBackend is not available. You may install sage-attention"
|
"SageAttentionBackend is not available. You may install sage-attention"
|
||||||
" by pip install git+https://github.com/thu-ml/SageAttention.git"
|
" by pip install git+https://github.com/thu-ml/SageAttention.git"
|
||||||
)
|
)
|
||||||
raise ImportError
|
sageattn = None
|
||||||
|
|
||||||
# TODO add sage3 attention backend
|
# TODO add sage3 attention backend
|
||||||
|
|
||||||
@@ -92,6 +92,13 @@ class SageAttentionImpl(AttentionImpl):
|
|||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata = None,
|
attn_metadata: AttentionMetadata = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
|
if attn_metadata is not None and attn_metadata.attn_mask is not None:
|
||||||
|
raise ValueError("SAGE_ATTN does not support attn_mask. Select a mask-capable backend.")
|
||||||
|
if sageattn is None:
|
||||||
|
raise ImportError(
|
||||||
|
"SAGE_ATTN requires sageattention. Install with: "
|
||||||
|
"pip install git+https://github.com/thu-ml/SageAttention.git"
|
||||||
|
)
|
||||||
output = sageattn(
|
output = sageattn(
|
||||||
query,
|
query,
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
|
||||||
from vllm.logger import init_logger
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
from vllm_omni.diffusion.attention.backends.abstract import (
|
from vllm_omni.diffusion.attention.backends.abstract import (
|
||||||
@@ -65,8 +66,6 @@ class SageAttention3Backend(AttentionBackend):
|
|||||||
|
|
||||||
|
|
||||||
class SageAttention3Impl(AttentionImpl):
|
class SageAttention3Impl(AttentionImpl):
|
||||||
_warned_gqa_fallback_global: bool = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
num_heads: int,
|
num_heads: int,
|
||||||
@@ -80,6 +79,12 @@ class SageAttention3Impl(AttentionImpl):
|
|||||||
self.causal = causal
|
self.causal = causal
|
||||||
self.softmax_scale = softmax_scale
|
self.softmax_scale = softmax_scale
|
||||||
self.dropout = extra_impl_args.get("dropout_p", 0.0)
|
self.dropout = extra_impl_args.get("dropout_p", 0.0)
|
||||||
|
expected_scale = head_size**-0.5
|
||||||
|
if not math.isclose(softmax_scale, expected_scale, rel_tol=1e-6):
|
||||||
|
raise ValueError(
|
||||||
|
"SAGE_ATTN_3 does not expose a custom softmax scale; "
|
||||||
|
f"expected {expected_scale} for head_size={head_size}, got {softmax_scale}."
|
||||||
|
)
|
||||||
|
|
||||||
def forward_cuda(
|
def forward_cuda(
|
||||||
self,
|
self,
|
||||||
@@ -88,6 +93,8 @@ class SageAttention3Impl(AttentionImpl):
|
|||||||
value: torch.Tensor,
|
value: torch.Tensor,
|
||||||
attn_metadata: AttentionMetadata | None = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
|
if attn_metadata is not None and attn_metadata.attn_mask is not None:
|
||||||
|
raise ValueError("SAGE_ATTN_3 does not support attn_mask. Select a mask-capable backend.")
|
||||||
query = query.transpose(1, 2).contiguous()
|
query = query.transpose(1, 2).contiguous()
|
||||||
key = key.transpose(1, 2).contiguous()
|
key = key.transpose(1, 2).contiguous()
|
||||||
value = value.transpose(1, 2).contiguous()
|
value = value.transpose(1, 2).contiguous()
|
||||||
@@ -98,19 +105,11 @@ class SageAttention3Impl(AttentionImpl):
|
|||||||
"GQA/MQA requires query heads to be a multiple of KV heads, "
|
"GQA/MQA requires query heads to be a multiple of KV heads, "
|
||||||
f"got q_heads={query.shape[1]} and kv_heads={key.shape[1]}"
|
f"got q_heads={query.shape[1]} and kv_heads={key.shape[1]}"
|
||||||
)
|
)
|
||||||
if not type(self)._warned_gqa_fallback_global:
|
raise NotImplementedError(
|
||||||
logger.warning("SageAttention3 does not support GQA/MQA (Hq != Hkv); falling back to torch SDPA.")
|
"SAGE_ATTN_3 was explicitly selected but does not support GQA/MQA "
|
||||||
type(self)._warned_gqa_fallback_global = True
|
f"(q_heads={query.shape[1]}, kv_heads={key.shape[1]}). Select a GQA-capable backend."
|
||||||
output = F.scaled_dot_product_attention(
|
|
||||||
query,
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
is_causal=self.causal,
|
|
||||||
dropout_p=self.dropout,
|
|
||||||
scale=self.softmax_scale,
|
|
||||||
enable_gqa=True,
|
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
output = _sageattn3_blackwell_op(query, key, value, self.causal)
|
output = _sageattn3_blackwell_op(query, key, value, self.causal)
|
||||||
|
|
||||||
return output.transpose(1, 2).contiguous()
|
return output.transpose(1, 2).contiguous()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
@@ -56,7 +56,8 @@ class SDPABackend(AttentionBackend):
|
|||||||
accept_output_buffer: bool = True
|
accept_output_buffer: bool = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_attention_mask(cls) -> bool:
|
def supports_attention_mask(cls, attention_spec: object | None = None) -> bool:
|
||||||
|
del attention_spec
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
# Copyright 2025 The Fairseq Authors and the HuggingFace Inc. team. All rights reserved.
|
# Copyright 2025 The Fairseq Authors and the HuggingFace Inc. team. All rights reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -12,7 +15,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
# Adapted from https://github.com/huggingface/transformers/blob/main/src/transformers/modeling_flash_attention_utils.py
|
# Adapted from https://github.com/huggingface/transformers/blob/main/src/transformers/modeling_flash_attention_utils.py
|
||||||
|
from collections.abc import Callable
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
@@ -20,6 +25,8 @@ from vllm.logger import init_logger
|
|||||||
|
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
|
FlashAttnFn = Callable[..., Any]
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,9 +38,27 @@ def _is_blackwell() -> bool:
|
|||||||
return capability is not None and capability.major >= 10
|
return capability is not None and capability.major >= 10
|
||||||
|
|
||||||
|
|
||||||
# Flash Attention function detection with fallback chain
|
@lru_cache(maxsize=1)
|
||||||
flash_attn_func = None
|
def is_flash_attn_4_available() -> bool:
|
||||||
flash_attn_varlen_func = None
|
"""Return whether CuTe FlashAttention-4 (``flash_attn.cute``) can be imported.
|
||||||
|
|
||||||
|
FA2/FA3 wheels are often importable on Blackwell but only ship Hopper kernels.
|
||||||
|
Those must not be treated as a runnable FLASH_ATTN implementation here.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from flash_attn.cute import flash_attn_varlen_func # noqa: F401
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("CuTe FlashAttention-4 is unavailable: %s", exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Flash Attention implementation discovery. All candidates implement the same
|
||||||
|
# logical FLASH_ATTN backend; this never substitutes SDPA at runtime.
|
||||||
|
# Bind via aliases so mypy does not treat each candidate import as a redefinition.
|
||||||
|
flash_attn_func: FlashAttnFn | None = None
|
||||||
|
flash_attn_varlen_func: FlashAttnFn | None = None
|
||||||
|
|
||||||
if current_omni_platform.is_rocm():
|
if current_omni_platform.is_rocm():
|
||||||
# ROCm: try Aiter first
|
# ROCm: try Aiter first
|
||||||
@@ -41,7 +66,11 @@ if current_omni_platform.is_rocm():
|
|||||||
from vllm._aiter_ops import is_aiter_found_and_supported
|
from vllm._aiter_ops import is_aiter_found_and_supported
|
||||||
|
|
||||||
if is_aiter_found_and_supported():
|
if is_aiter_found_and_supported():
|
||||||
from aiter import flash_attn_func, flash_attn_varlen_func # noqa: F401
|
from aiter import flash_attn_func as _fa_func
|
||||||
|
from aiter import flash_attn_varlen_func as _fa_varlen
|
||||||
|
|
||||||
|
flash_attn_func = _fa_func
|
||||||
|
flash_attn_varlen_func = _fa_varlen
|
||||||
except (ImportError, ModuleNotFoundError):
|
except (ImportError, ModuleNotFoundError):
|
||||||
pass
|
pass
|
||||||
elif current_omni_platform.is_xpu():
|
elif current_omni_platform.is_xpu():
|
||||||
@@ -53,64 +82,84 @@ elif current_omni_platform.is_xpu():
|
|||||||
pass
|
pass
|
||||||
elif current_omni_platform.is_musa():
|
elif current_omni_platform.is_musa():
|
||||||
try:
|
try:
|
||||||
from flash_attn_interface import flash_attn_func, flash_attn_varlen_func # noqa: F401
|
from flash_attn_interface import flash_attn_func as _fa_func
|
||||||
|
from flash_attn_interface import flash_attn_varlen_func as _fa_varlen
|
||||||
|
|
||||||
|
flash_attn_func = _fa_func
|
||||||
|
flash_attn_varlen_func = _fa_varlen
|
||||||
except (ImportError, ModuleNotFoundError):
|
except (ImportError, ModuleNotFoundError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# CUDA: prefer the CuTe-based FA4 package when present. This matters on
|
# CUDA implementation candidates, ordered by preference.
|
||||||
# Blackwell, where an importable Hopper-only FA3 wheel may otherwise be
|
# Candidate 1: CuTe-based FA4. This matters on Blackwell, where an
|
||||||
# selected and fail at launch with "no kernel image is available".
|
# importable Hopper-only FA3 wheel may otherwise be selected and fail
|
||||||
|
# at launch with "no kernel image is available".
|
||||||
if _is_blackwell():
|
if _is_blackwell():
|
||||||
try:
|
if is_flash_attn_4_available():
|
||||||
from flash_attn.cute import flash_attn_func, flash_attn_varlen_func # noqa: F401
|
try:
|
||||||
|
from flash_attn.cute import flash_attn_func as _fa_func
|
||||||
|
from flash_attn.cute import flash_attn_varlen_func as _fa_varlen
|
||||||
|
|
||||||
logger.info("Using CuTe FlashAttention-4 on Blackwell")
|
flash_attn_func = _fa_func
|
||||||
except Exception as exc:
|
flash_attn_varlen_func = _fa_varlen
|
||||||
# Optional FA4 dependencies may be present but ABI-incompatible
|
logger.info("Using CuTe FlashAttention-4 on Blackwell")
|
||||||
# (for example, mismatched CUTLASS DSL and Quack builds). Treat
|
except Exception as exc:
|
||||||
# that as unavailable so backend discovery can continue.
|
# Optional FA4 dependencies may be present but ABI-incompatible
|
||||||
logger.debug("CuTe FlashAttention-4 is unavailable: %s", exc)
|
# (for example, mismatched CUTLASS DSL and Quack builds). Treat
|
||||||
|
# that as unavailable so backend discovery can continue.
|
||||||
|
logger.debug("CuTe FlashAttention-4 is unavailable: %s", exc)
|
||||||
|
else:
|
||||||
|
# Candidate 2: FA3 from fa3-fwd PyPI package.
|
||||||
|
if flash_attn_func is None:
|
||||||
|
try:
|
||||||
|
from fa3_fwd_interface import flash_attn_func as _fa_func
|
||||||
|
from fa3_fwd_interface import flash_attn_varlen_func as _fa_varlen
|
||||||
|
|
||||||
# Try FA3 -> FA2 fallback chain.
|
flash_attn_func = _fa_func
|
||||||
# Try FA3 from fa3-fwd PyPI package
|
flash_attn_varlen_func = _fa_varlen
|
||||||
if flash_attn_func is None:
|
except (ImportError, ModuleNotFoundError):
|
||||||
try:
|
pass
|
||||||
from fa3_fwd_interface import flash_attn_func, flash_attn_varlen_func # noqa: F401
|
|
||||||
except (ImportError, ModuleNotFoundError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fallback: Try FA3 from flash-attention source build
|
# Candidate 3: FA3 from a flash-attention source build.
|
||||||
if flash_attn_func is None:
|
if flash_attn_func is None:
|
||||||
try:
|
try:
|
||||||
from flash_attn_interface import flash_attn_func, flash_attn_varlen_func # noqa: F401
|
from flash_attn_interface import flash_attn_func as _fa_func
|
||||||
except (ImportError, ModuleNotFoundError):
|
from flash_attn_interface import flash_attn_varlen_func as _fa_varlen
|
||||||
pass
|
|
||||||
|
|
||||||
# Fallback: Try FA2 from flash-attn package (try multiple import paths)
|
flash_attn_func = _fa_func
|
||||||
if flash_attn_func is None:
|
flash_attn_varlen_func = _fa_varlen
|
||||||
try:
|
except (ImportError, ModuleNotFoundError):
|
||||||
from flash_attn import flash_attn_func, flash_attn_varlen_func # noqa: F401
|
pass
|
||||||
except (ImportError, ModuleNotFoundError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
if flash_attn_func is None:
|
# Candidate 4: FA2 from the flash-attn package (multiple import paths).
|
||||||
try:
|
if flash_attn_func is None:
|
||||||
from flash_attn.flash_attn_interface import ( # noqa: F401
|
try:
|
||||||
flash_attn_func,
|
from flash_attn import flash_attn_func as _fa_func
|
||||||
flash_attn_varlen_func,
|
from flash_attn import flash_attn_varlen_func as _fa_varlen
|
||||||
)
|
|
||||||
except (ImportError, ModuleNotFoundError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fallback: Try vLLM's encapsulated flash attention dispatcher
|
flash_attn_func = _fa_func
|
||||||
# TODO discuss priority and remove potentially redundant fallbacks
|
flash_attn_varlen_func = _fa_varlen
|
||||||
if flash_attn_varlen_func is None:
|
except (ImportError, ModuleNotFoundError):
|
||||||
try:
|
pass
|
||||||
from vllm.vllm_flash_attn import ( # noqa: F401
|
|
||||||
flash_attn_varlen_func,
|
if flash_attn_func is None:
|
||||||
)
|
try:
|
||||||
except (ImportError, ModuleNotFoundError):
|
from flash_attn.flash_attn_interface import flash_attn_func as _fa_func
|
||||||
pass
|
from flash_attn.flash_attn_interface import flash_attn_varlen_func as _fa_varlen
|
||||||
|
|
||||||
|
flash_attn_func = _fa_func
|
||||||
|
flash_attn_varlen_func = _fa_varlen
|
||||||
|
except (ImportError, ModuleNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Candidate 5: vLLM's encapsulated Flash Attention dispatcher.
|
||||||
|
if flash_attn_varlen_func is None:
|
||||||
|
try:
|
||||||
|
from vllm.vllm_flash_attn import flash_attn_varlen_func as _fa_varlen
|
||||||
|
|
||||||
|
flash_attn_varlen_func = _fa_varlen
|
||||||
|
except (ImportError, ModuleNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
# If no FA backend available, SDPA backend will be selected at the platform level
|
# If no FA backend available, SDPA backend will be selected at the platform level
|
||||||
# flash_attn_func and flash_attn_varlen_func will be None
|
# flash_attn_func and flash_attn_varlen_func will be None
|
||||||
@@ -127,12 +176,7 @@ def is_flash_attn_installed() -> bool:
|
|||||||
# Check for any FA backend: FA4 (flash_attn.cute), FA3
|
# Check for any FA backend: FA4 (flash_attn.cute), FA3
|
||||||
# (fa3_fwd_interface, flash_attn_interface), or FA2 (flash_attn).
|
# (fa3_fwd_interface, flash_attn_interface), or FA2 (flash_attn).
|
||||||
if _is_blackwell():
|
if _is_blackwell():
|
||||||
try:
|
return is_flash_attn_4_available()
|
||||||
from flash_attn.cute import flash_attn_varlen_func # noqa: F401
|
|
||||||
|
|
||||||
return True
|
|
||||||
except Exception as exc:
|
|
||||||
logger.debug("CuTe FlashAttention-4 is unavailable: %s", exc)
|
|
||||||
|
|
||||||
# Try FA3 from fa3-fwd PyPI package
|
# Try FA3 from fa3-fwd PyPI package
|
||||||
try:
|
try:
|
||||||
@@ -167,7 +211,7 @@ def is_flash_attn_installed() -> bool:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
except (ImportError, ModuleNotFoundError):
|
except (ImportError, ModuleNotFoundError):
|
||||||
logger.warning("No Flash Attention backend found, using pytorch SDPA implementation")
|
logger.warning("No Flash Attention implementation found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class Attention(nn.Module):
|
|||||||
# set_current_diffusion_config(); no env-var re-parsing needed here.
|
# set_current_diffusion_config(); no env-var re-parsing needed here.
|
||||||
backend_kwargs: dict | None = None
|
backend_kwargs: dict | None = None
|
||||||
self.backend_pref = None
|
self.backend_pref = None
|
||||||
|
self.backend_explicit = False
|
||||||
|
|
||||||
config = get_current_diffusion_config_or_none()
|
config = get_current_diffusion_config_or_none()
|
||||||
attention_config = config.diffusion_attention_config if config is not None else None
|
attention_config = config.diffusion_attention_config if config is not None else None
|
||||||
@@ -129,22 +130,17 @@ class Attention(nn.Module):
|
|||||||
# FLASH_ATTN is an Omni selector, not a device-specific kernel.
|
# FLASH_ATTN is an Omni selector, not a device-specific kernel.
|
||||||
# vLLM resolves it to CUDA FlashAttention on GPU and Ascend native
|
# vLLM resolves it to CUDA FlashAttention on GPU and Ascend native
|
||||||
# attention on NPU. A platform may legitimately default dense
|
# attention on NPU. A platform may legitimately default dense
|
||||||
# attention to SDPA (for example when an optional diffusion FA
|
# attention to CUDNN/SDPA (for example when optional dense FA
|
||||||
# package is absent), but a paged layer must advertise the
|
# extras are absent), but a paged layer must advertise the
|
||||||
# capability so the Worker can register its native cache view.
|
# capability so the Worker can register its native cache view.
|
||||||
# Resolve the selector only for this marked layer; unmarked dense
|
# Load that selector from the registry: this is not a user-explicit
|
||||||
# attention keeps the platform default unchanged.
|
# dense FLASH_ATTN request, so skip platform explicit-backend
|
||||||
from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec
|
# validation (Blackwell would otherwise require CuTe FA4). Formal
|
||||||
|
# paged execution still delegates to vLLM's native paged backend.
|
||||||
|
from vllm_omni.diffusion.attention.backends.registry import DiffusionAttentionBackendEnum
|
||||||
|
|
||||||
dense_backend_name = attn_backend_cls.get_name()
|
dense_backend_name = attn_backend_cls.get_name()
|
||||||
paged_attention_config = AttentionConfig(default=AttentionSpec(backend="FLASH_ATTN"))
|
attn_backend_cls = DiffusionAttentionBackendEnum.FLASH_ATTN.get_class()
|
||||||
attn_backend_cls, spec = get_attn_backend_for_role(
|
|
||||||
role=role,
|
|
||||||
head_size=head_size,
|
|
||||||
attention_config=paged_attention_config,
|
|
||||||
role_category=role_category,
|
|
||||||
allow_trtllm_default=allow_trtllm_default,
|
|
||||||
)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Resolved marked paged diffusion attention role=%r to %r because the platform default %r "
|
"Resolved marked paged diffusion attention role=%r to %r because the platform default %r "
|
||||||
"does not support Scheduler-owned KV",
|
"does not support Scheduler-owned KV",
|
||||||
@@ -161,12 +157,17 @@ class Attention(nn.Module):
|
|||||||
"TRTLLM_ATTN does not support AllGather-KV sequence parallelism. "
|
"TRTLLM_ATTN does not support AllGather-KV sequence parallelism. "
|
||||||
"Set --allgather-degree 1 or select another diffusion attention backend."
|
"Set --allgather-degree 1 or select another diffusion attention backend."
|
||||||
)
|
)
|
||||||
|
self.attn_spec = spec
|
||||||
if spec is not None:
|
if spec is not None:
|
||||||
backend_kwargs = spec.backend_kwargs()
|
backend_kwargs = spec.backend_kwargs()
|
||||||
self.backend_pref = spec.backend
|
self.backend_pref = spec.backend
|
||||||
|
self.backend_explicit = True
|
||||||
logger.debug("Attention(role=%s) → backend=%s", role, spec.backend)
|
logger.debug("Attention(role=%s) → backend=%s", role, spec.backend)
|
||||||
else:
|
else:
|
||||||
logger.debug("Attention(role=%s) → platform default", role)
|
# Propagate the resolved platform default so Ring Attention can
|
||||||
|
# make a compatible automatic selection on the current GPU.
|
||||||
|
self.backend_pref = attn_backend_cls.get_name()
|
||||||
|
logger.debug("Attention(role=%s) → platform default (%s)", role, self.backend_pref)
|
||||||
|
|
||||||
self.attn_backend = attn_backend_cls
|
self.attn_backend = attn_backend_cls
|
||||||
self.attn_impl_cls = self.attn_backend.get_impl_cls()
|
self.attn_impl_cls = self.attn_backend.get_impl_cls()
|
||||||
@@ -180,8 +181,10 @@ class Attention(nn.Module):
|
|||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
backend_kwargs=backend_kwargs,
|
backend_kwargs=backend_kwargs,
|
||||||
role=role,
|
role=role,
|
||||||
|
backend_explicit=self.backend_explicit,
|
||||||
)
|
)
|
||||||
# Instantiate fallback backend for float32 support
|
# Some model-specific paths call this helper directly. The main backend
|
||||||
|
# dispatch below never switches to it implicitly.
|
||||||
self.sdpa_fallback = SDPABackend.get_impl_cls()(
|
self.sdpa_fallback = SDPABackend.get_impl_cls()(
|
||||||
num_heads=num_heads,
|
num_heads=num_heads,
|
||||||
head_size=head_size,
|
head_size=head_size,
|
||||||
@@ -205,16 +208,13 @@ class Attention(nn.Module):
|
|||||||
if config is not None:
|
if config is not None:
|
||||||
if config.parallel_config.ring_degree > 1:
|
if config.parallel_config.ring_degree > 1:
|
||||||
self.use_ring = True
|
self.use_ring = True
|
||||||
try:
|
sp_group = get_sp_group()
|
||||||
sp_group = get_sp_group()
|
self.ring_pg = sp_group.ring_group
|
||||||
self.ring_pg = sp_group.ring_group
|
self.ring_runner = RingParallelAttention(
|
||||||
self.ring_runner = RingParallelAttention(
|
sp_group,
|
||||||
sp_group,
|
attn_backend_pref=self.backend_pref,
|
||||||
attn_backend_pref=self.backend_pref,
|
attn_backend_explicit=self.backend_explicit,
|
||||||
)
|
)
|
||||||
except Exception:
|
|
||||||
self.use_ring = False
|
|
||||||
self.ring_runner = None
|
|
||||||
|
|
||||||
self.parallel_strategy = build_parallel_attention_strategy(
|
self.parallel_strategy = build_parallel_attention_strategy(
|
||||||
scatter_idx=scatter_idx,
|
scatter_idx=scatter_idx,
|
||||||
@@ -222,7 +222,7 @@ class Attention(nn.Module):
|
|||||||
use_sync=use_sync,
|
use_sync=use_sync,
|
||||||
causal=causal,
|
causal=causal,
|
||||||
)
|
)
|
||||||
# Fallback strategy when SP is not active (outside sharded regions)
|
# Local strategy when SP is intentionally inactive outside sharded regions.
|
||||||
self._no_parallel_strategy = NoParallelAttention()
|
self._no_parallel_strategy = NoParallelAttention()
|
||||||
|
|
||||||
self.layer_idx: int | None = _try_extract_layer_index(prefix)
|
self.layer_idx: int | None = _try_extract_layer_index(prefix)
|
||||||
@@ -285,14 +285,11 @@ class Attention(nn.Module):
|
|||||||
)
|
)
|
||||||
platform_key = current_omni_platform.device_name
|
platform_key = current_omni_platform.device_name
|
||||||
if not self.attention.supports_kv_cache_dtype(dtype, platform_key):
|
if not self.attention.supports_kv_cache_dtype(dtype, platform_key):
|
||||||
logger.warning_once(
|
raise ValueError(
|
||||||
"Attention backend %s does not support kv_cache_dtype='%s' on %s. "
|
f"Attention backend {self.attn_backend.get_name()} does not support "
|
||||||
"KV quantization will be disabled.",
|
f"kv_cache_dtype={dtype!r} on {platform_key}. Select a compatible "
|
||||||
self.attn_backend.get_name(),
|
"backend or set diffusion_kv_cache_dtype='auto'."
|
||||||
dtype,
|
|
||||||
platform_key,
|
|
||||||
)
|
)
|
||||||
dtype = None
|
|
||||||
self._kv_cache_dtype = dtype
|
self._kv_cache_dtype = dtype
|
||||||
self._kv_cache_skip_steps = getattr(config, "diffusion_kv_cache_skip_step_indices", None)
|
self._kv_cache_skip_steps = getattr(config, "diffusion_kv_cache_skip_step_indices", None)
|
||||||
self._kv_cache_skip_layers = getattr(config, "diffusion_kv_cache_skip_layer_indices", None)
|
self._kv_cache_skip_layers = getattr(config, "diffusion_kv_cache_skip_layer_indices", None)
|
||||||
@@ -470,42 +467,44 @@ class Attention(nn.Module):
|
|||||||
return self.paged_kv_cache_role is not None and self._active_paged_kv_adapter() is not None
|
return self.paged_kv_cache_role is not None and self._active_paged_kv_adapter() is not None
|
||||||
|
|
||||||
def _run_local_attention(self, query, key, value, attn_metadata):
|
def _run_local_attention(self, query, key, value, attn_metadata):
|
||||||
self._assert_piecewise_compatible(attn_metadata)
|
self._assert_metadata_compatible(attn_metadata)
|
||||||
|
|
||||||
if query.dtype == torch.float32:
|
|
||||||
logger.warning_once(
|
|
||||||
f"Only SDPA supports float32. Overriding user config {type(self.attention)} "
|
|
||||||
f"attention_backend='{self.backend_pref}' to 'sdpa' for dtype={query.dtype}."
|
|
||||||
)
|
|
||||||
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
|
|
||||||
|
|
||||||
in_kv_memory_profile = is_forward_context_available() and get_forward_context().in_diffusion_kv_memory_profile
|
in_kv_memory_profile = is_forward_context_available() and get_forward_context().in_diffusion_kv_memory_profile
|
||||||
# NPU only: the startup KV-capacity profile needs tensor shapes, not a
|
# The startup KV-capacity profile needs tensor shapes, not a paged
|
||||||
# paged attention result. If dense MindIE-SD is absent, SDPA provides
|
# attention result. If dense FLASH_ATTN deps are absent (NPU MindIE-SD
|
||||||
# that profile forward. Formal GPU/NPU paged requests never use this
|
# or CUDA CuTe FA4), SDPA provides that profile forward. Formal paged
|
||||||
# branch because their Worker adapter is active.
|
# requests never use this branch because their Worker adapter is active.
|
||||||
|
# Do not silently remap float32 (or other dtypes) to SDPA: a
|
||||||
|
# user-explicit backend must run or raise.
|
||||||
if (
|
if (
|
||||||
self._scheduler_paged_kv
|
self._scheduler_paged_kv
|
||||||
and self.paged_kv_cache_role is not None
|
and self.paged_kv_cache_role is not None
|
||||||
and in_kv_memory_profile
|
and in_kv_memory_profile
|
||||||
and current_omni_platform.is_npu()
|
|
||||||
and self.attn_backend.get_name() == "FLASH_ATTN"
|
and self.attn_backend.get_name() == "FLASH_ATTN"
|
||||||
and not current_omni_platform.supports_diffusion_dense_flash_attention()
|
and not current_omni_platform.supports_diffusion_dense_flash_attention()
|
||||||
):
|
):
|
||||||
logger.warning_once(
|
logger.warning_once(
|
||||||
"The startup KV memory profile is using SDPA because MindIE-SD is unavailable. "
|
"The startup KV memory profile is using SDPA because dense FLASH_ATTN is unavailable. "
|
||||||
"Formal paged requests still use the platform-native paged attention backend."
|
"Formal paged requests still use the platform-native paged attention backend."
|
||||||
)
|
)
|
||||||
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
|
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
|
||||||
|
|
||||||
return self.attention.forward(query, key, value, attn_metadata)
|
return self.attention.forward(query, key, value, attn_metadata)
|
||||||
|
|
||||||
def _assert_piecewise_compatible(self, attn_metadata: AttentionMetadata | None) -> None:
|
def _assert_metadata_compatible(self, attn_metadata: AttentionMetadata | None) -> None:
|
||||||
if attn_metadata is None or attn_metadata.full_attn_spans is None:
|
if attn_metadata is None:
|
||||||
|
return
|
||||||
|
backend_name = self.attn_backend.get_name()
|
||||||
|
if attn_metadata.attn_mask is not None and not self.attn_backend.supports_attention_mask(
|
||||||
|
getattr(self, "attn_spec", None)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"Attention backend '{backend_name}' does not support attn_mask. Select a mask-capable backend."
|
||||||
|
)
|
||||||
|
if attn_metadata.full_attn_spans is None:
|
||||||
return
|
return
|
||||||
if attn_metadata.attn_mask is not None and attn_metadata.attn_mask.ndim == 4:
|
if attn_metadata.attn_mask is not None and attn_metadata.attn_mask.ndim == 4:
|
||||||
return
|
return
|
||||||
backend_name = self.attn_backend.get_name()
|
|
||||||
if not self.attn_backend.supports_piecewise_spans:
|
if not self.attn_backend.supports_piecewise_spans:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Attention backend '{backend_name}' does not support "
|
f"Attention backend '{backend_name}' does not support "
|
||||||
@@ -515,6 +514,8 @@ class Attention(nn.Module):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _run_ring_attention(self, query, key, value, attn_metadata):
|
def _run_ring_attention(self, query, key, value, attn_metadata):
|
||||||
|
if attn_metadata is not None and attn_metadata.attn_mask is not None:
|
||||||
|
raise ValueError("Ring attention does not support attn_mask; use Ulysses SP or disable mask_sp_padding.")
|
||||||
skip = getattr(self.attention, "skip", None)
|
skip = getattr(self.attention, "skip", None)
|
||||||
if skip is not None and getattr(skip, "configured", False):
|
if skip is not None and getattr(skip, "configured", False):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from vllm_omni.diffusion.distributed.parallel_state import (
|
|||||||
get_sequence_parallel_world_size,
|
get_sequence_parallel_world_size,
|
||||||
get_sp_group,
|
get_sp_group,
|
||||||
)
|
)
|
||||||
from vllm_omni.diffusion.forward_context import get_forward_context
|
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
@@ -37,32 +37,32 @@ def build_parallel_attention_strategy(
|
|||||||
- Parallel attention selection is handled here, based on distributed config
|
- Parallel attention selection is handled here, based on distributed config
|
||||||
and initialized process groups.
|
and initialized process groups.
|
||||||
"""
|
"""
|
||||||
try:
|
if not is_forward_context_available():
|
||||||
cfg = get_forward_context().omni_diffusion_config
|
|
||||||
p = cfg.parallel_config
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"No forward context available for parallel attention strategy: {e}")
|
|
||||||
return NoParallelAttention()
|
return NoParallelAttention()
|
||||||
|
cfg = get_forward_context().omni_diffusion_config
|
||||||
|
p = cfg.parallel_config
|
||||||
|
|
||||||
ulysses_degree = getattr(p, "ulysses_degree", 1)
|
ulysses_degree = getattr(p, "ulysses_degree", 1)
|
||||||
ring_degree = getattr(p, "ring_degree", 1)
|
ring_degree = getattr(p, "ring_degree", 1)
|
||||||
allgather_degree = getattr(p, "allgather_degree", 1)
|
allgather_degree = getattr(p, "allgather_degree", 1)
|
||||||
ulysses_a2a_permute = getattr(p, "ulysses_a2a_permute", False)
|
ulysses_a2a_permute = getattr(p, "ulysses_a2a_permute", False)
|
||||||
|
|
||||||
|
sp_configured = ulysses_degree > 1 or ring_degree > 1 or allgather_degree > 1
|
||||||
|
if not sp_configured:
|
||||||
|
return NoParallelAttention()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sp_group = get_sp_group()
|
sp_group = get_sp_group()
|
||||||
# Ensure SP group is initialized and world size > 1
|
|
||||||
if get_sequence_parallel_world_size() <= 1:
|
|
||||||
return NoParallelAttention()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log warning if SP is configured but group is not available
|
raise RuntimeError(
|
||||||
if ulysses_degree > 1 or ring_degree > 1 or allgather_degree > 1:
|
f"SP is configured (ulysses={ulysses_degree}, ring={ring_degree}, "
|
||||||
logger.warning(
|
f"allgather={allgather_degree}), but the SP group is unavailable."
|
||||||
f"SP configured (ulysses={ulysses_degree}, ring={ring_degree}, "
|
) from e
|
||||||
f"allgather={allgather_degree}) but SP group not available: {e}. "
|
if get_sequence_parallel_world_size() <= 1:
|
||||||
f"Falling back to NoParallelAttention. This may cause incorrect results."
|
raise RuntimeError(
|
||||||
)
|
f"SP is configured (ulysses={ulysses_degree}, ring={ring_degree}, "
|
||||||
return NoParallelAttention()
|
f"allgather={allgather_degree}), but the initialized SP world size is not greater than one."
|
||||||
|
)
|
||||||
|
|
||||||
if allgather_degree > 1:
|
if allgather_degree > 1:
|
||||||
if causal:
|
if causal:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -10,7 +10,13 @@ import torch
|
|||||||
from vllm.logger import init_logger
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
# import torch.distributed as dist # Not used directly here, but good practice if needed
|
# import torch.distributed as dist # Not used directly here, but good practice if needed
|
||||||
from vllm_omni.diffusion.attention.backends.ring.ring_globals import HAS_AITER, HAS_FA3, HAS_FA4, HAS_FLASH_ATTN
|
from vllm_omni.diffusion.attention.backends.ring.ring_globals import (
|
||||||
|
FA3_SUPPORTED_CUDA_MAJORS,
|
||||||
|
HAS_AITER,
|
||||||
|
HAS_FA3,
|
||||||
|
HAS_FA4,
|
||||||
|
HAS_FLASH_ATTN,
|
||||||
|
)
|
||||||
from vllm_omni.diffusion.attention.backends.ring.ring_selector import AttnType
|
from vllm_omni.diffusion.attention.backends.ring.ring_selector import AttnType
|
||||||
from vllm_omni.diffusion.attention.parallel.base import (
|
from vllm_omni.diffusion.attention.parallel.base import (
|
||||||
ParallelAttentionContext,
|
ParallelAttentionContext,
|
||||||
@@ -24,6 +30,41 @@ if TYPE_CHECKING:
|
|||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_fa3(device: torch.device) -> bool:
|
||||||
|
"""Return whether the installed FA3 kernels support ``device``.
|
||||||
|
|
||||||
|
Importing an extension only proves that its Python module is installed.
|
||||||
|
When the source publishes a supported-major contract, also check it before
|
||||||
|
entering a CUDA launcher that may abort instead of raising an exception.
|
||||||
|
"""
|
||||||
|
if not HAS_FA3 or device.type != "cuda":
|
||||||
|
return False
|
||||||
|
if FA3_SUPPORTED_CUDA_MAJORS is None:
|
||||||
|
return True
|
||||||
|
major, _minor = torch.cuda.get_device_capability(device)
|
||||||
|
return major in FA3_SUPPORTED_CUDA_MAJORS
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_fa4(device: torch.device) -> bool:
|
||||||
|
"""Return whether the installed FA4 kernels support ``device``."""
|
||||||
|
if not HAS_FA4 or device.type != "cuda":
|
||||||
|
return False
|
||||||
|
major, _minor = torch.cuda.get_device_capability(device)
|
||||||
|
return major >= 10
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_fa2(device: torch.device) -> bool:
|
||||||
|
"""Return whether the FA2 backend supports ``device``.
|
||||||
|
|
||||||
|
FA2's CUDA backend supports Ampere, Ada, and Hopper. Blackwell support is
|
||||||
|
provided by the separate FA4 implementation.
|
||||||
|
"""
|
||||||
|
if not HAS_FLASH_ATTN or device.type != "cuda":
|
||||||
|
return False
|
||||||
|
major, _minor = torch.cuda.get_device_capability(device)
|
||||||
|
return major in (8, 9)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class _RingCtx(ParallelAttentionContext):
|
class _RingCtx(ParallelAttentionContext):
|
||||||
"""Per-forward context for Ring sequence-parallel attention."""
|
"""Per-forward context for Ring sequence-parallel attention."""
|
||||||
@@ -46,9 +87,11 @@ class RingParallelAttention:
|
|||||||
self,
|
self,
|
||||||
sp_group: SequenceParallelGroupCoordinator,
|
sp_group: SequenceParallelGroupCoordinator,
|
||||||
attn_backend_pref: str | None = None,
|
attn_backend_pref: str | None = None,
|
||||||
|
attn_backend_explicit: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._sp_group = sp_group
|
self._sp_group = sp_group
|
||||||
self.attn_backend_pref = attn_backend_pref
|
self.attn_backend_pref = attn_backend_pref
|
||||||
|
self.attn_backend_explicit = attn_backend_explicit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
@@ -110,14 +153,6 @@ class RingParallelAttention:
|
|||||||
if backend_pref is not None:
|
if backend_pref is not None:
|
||||||
backend_pref = backend_pref.lower()
|
backend_pref = backend_pref.lower()
|
||||||
|
|
||||||
# FP32 is not supported by Flash Attention, force SDPA
|
|
||||||
if query.dtype == torch.float32:
|
|
||||||
backend_pref = "sdpa"
|
|
||||||
elif not HAS_FA3 and not HAS_FLASH_ATTN and not HAS_AITER:
|
|
||||||
if backend_pref != "sdpa":
|
|
||||||
logger.warning_once("Flash Attention (FA2/FA3/AITER) is not available! Force enabling SDPA.")
|
|
||||||
backend_pref = "sdpa"
|
|
||||||
|
|
||||||
# Extract joint tensors
|
# Extract joint tensors
|
||||||
joint_key, joint_value = None, None
|
joint_key, joint_value = None, None
|
||||||
joint_strategy = "front"
|
joint_strategy = "front"
|
||||||
@@ -127,7 +162,7 @@ class RingParallelAttention:
|
|||||||
if attn_metadata.joint_strategy is not None:
|
if attn_metadata.joint_strategy is not None:
|
||||||
joint_strategy = attn_metadata.joint_strategy
|
joint_strategy = attn_metadata.joint_strategy
|
||||||
|
|
||||||
if backend_pref in {"sdpa", "torch", "torch_sdpa"}:
|
def _run_sdpa() -> torch.Tensor:
|
||||||
from vllm_omni.diffusion.attention.backends.ring_pytorch_attn import ring_pytorch_attn_func
|
from vllm_omni.diffusion.attention.backends.ring_pytorch_attn import ring_pytorch_attn_func
|
||||||
|
|
||||||
return ring_pytorch_attn_func(
|
return ring_pytorch_attn_func(
|
||||||
@@ -143,19 +178,72 @@ class RingParallelAttention:
|
|||||||
joint_strategy=joint_strategy,
|
joint_strategy=joint_strategy,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ring only implements local Flash/AITER and SDPA kernels. HuggingFace
|
||||||
|
# Hub FA and other non-local backends can use the SDPA ring path only
|
||||||
|
# when they came from automatic platform selection. An explicit
|
||||||
|
# request must never silently substitute local FA4/FA3/FA2.
|
||||||
|
_sdpa_prefs = {
|
||||||
|
"sdpa",
|
||||||
|
"torch",
|
||||||
|
"torch_sdpa",
|
||||||
|
}
|
||||||
|
_non_ring_prefs = {
|
||||||
|
"cudnn_attn",
|
||||||
|
"flashinfer_attn",
|
||||||
|
"trtllm_attn",
|
||||||
|
"sage_attn",
|
||||||
|
"sage_attn_3",
|
||||||
|
"flash_attn_hub",
|
||||||
|
"flash_attn_3_hub",
|
||||||
|
}
|
||||||
|
if backend_pref in _sdpa_prefs:
|
||||||
|
return _run_sdpa()
|
||||||
|
if backend_pref in _non_ring_prefs:
|
||||||
|
if self.attn_backend_explicit:
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.attn_backend_pref} was explicitly selected, but ring sequence parallelism "
|
||||||
|
"has no implementation for that backend. Select TORCH_SDPA/FLASH_ATTN or use Ulysses SP."
|
||||||
|
)
|
||||||
|
return _run_sdpa()
|
||||||
|
|
||||||
|
if query.dtype == torch.float32:
|
||||||
|
if self.attn_backend_explicit:
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.attn_backend_pref} was explicitly selected for ring attention, "
|
||||||
|
"but its ring kernel does not support float32. Select TORCH_SDPA or use a supported dtype."
|
||||||
|
)
|
||||||
|
return _run_sdpa()
|
||||||
|
|
||||||
|
can_use_fa4 = _can_use_fa4(query.device)
|
||||||
|
can_use_fa3 = _can_use_fa3(query.device)
|
||||||
|
can_use_fa2 = _can_use_fa2(query.device)
|
||||||
|
if not can_use_fa4 and not can_use_fa3 and not can_use_fa2 and not HAS_AITER:
|
||||||
|
if self.attn_backend_explicit:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{self.attn_backend_pref} was explicitly selected, but no compatible ring kernel "
|
||||||
|
f"is available for device {query.device}."
|
||||||
|
)
|
||||||
|
logger.warning_once(
|
||||||
|
"Automatic ring backend selection chose TORCH_SDPA because no compatible "
|
||||||
|
"FA2/FA3/FA4/AITER ring kernel is available for this device."
|
||||||
|
)
|
||||||
|
return _run_sdpa()
|
||||||
|
|
||||||
from vllm_omni.diffusion.attention.backends.ring_flash_attn import ring_flash_attn_func
|
from vllm_omni.diffusion.attention.backends.ring_flash_attn import ring_flash_attn_func
|
||||||
|
|
||||||
# Prefer FA4 on Blackwell. An importable Hopper-only FA3 wheel can
|
# Prefer FA4 on Blackwell. An importable Hopper-only FA3 wheel can
|
||||||
# otherwise be selected and fail at launch with "no kernel image".
|
# otherwise be selected and fail at launch with "no kernel image".
|
||||||
if HAS_FA4 and query.is_cuda and torch.cuda.get_device_capability(query.device)[0] >= 10:
|
if can_use_fa4:
|
||||||
attn_type = AttnType.FA4
|
attn_type = AttnType.FA4
|
||||||
# Prefer FA3 over FA2 on Ampere/Ada/Hopper. On ROCm, use AITER.
|
# Prefer FA3 over FA2 on Ampere/Ada/Hopper. On ROCm, use AITER.
|
||||||
elif HAS_FA3:
|
elif can_use_fa3:
|
||||||
attn_type = AttnType.FA3
|
attn_type = AttnType.FA3
|
||||||
elif HAS_AITER:
|
elif HAS_AITER:
|
||||||
attn_type = AttnType.AITER
|
attn_type = AttnType.AITER
|
||||||
else:
|
elif can_use_fa2:
|
||||||
attn_type = AttnType.FA
|
attn_type = AttnType.FA
|
||||||
|
else:
|
||||||
|
raise RuntimeError("No compatible Flash Attention backend is available for ring attention.")
|
||||||
|
|
||||||
return ring_flash_attn_func(
|
return ring_flash_attn_func(
|
||||||
query,
|
query,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""
|
"""
|
||||||
Diffusion attention backend selector.
|
Diffusion attention backend selector.
|
||||||
|
|
||||||
@@ -25,6 +25,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
# SequenceParallel auto-pad probes mask support before Attention layers exist,
|
||||||
|
# so it has no real head_dim. Callers must not treat this sentinel as geometry.
|
||||||
|
HEAD_SIZE_UNKNOWN = -1
|
||||||
|
|
||||||
|
|
||||||
def _load_backend_cls(cls_path: str) -> type[AttentionBackend]:
|
def _load_backend_cls(cls_path: str) -> type[AttentionBackend]:
|
||||||
"""Load a backend class from its fully qualified path.
|
"""Load a backend class from its fully qualified path.
|
||||||
@@ -151,3 +155,29 @@ def get_attn_backend_for_role(
|
|||||||
source="platform default",
|
source="platform default",
|
||||||
)
|
)
|
||||||
return backend_cls, None
|
return backend_cls, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_attn_backend_for_capability(
|
||||||
|
role: str,
|
||||||
|
attention_config: AttentionConfig | None = None,
|
||||||
|
role_category: str | None = None,
|
||||||
|
) -> type[AttentionBackend]:
|
||||||
|
"""Resolve the backend class for capability queries such as mask support.
|
||||||
|
|
||||||
|
SequenceParallel auto-pad does not know ``head_size``. Explicit
|
||||||
|
``AttentionConfig`` selections are loaded from the registry without
|
||||||
|
geometry validation, so CUDNN_ATTN is not rejected for the unknown
|
||||||
|
sentinel. Platform defaults still consult the platform with
|
||||||
|
``HEAD_SIZE_UNKNOWN``.
|
||||||
|
"""
|
||||||
|
spec = None
|
||||||
|
if attention_config is not None:
|
||||||
|
spec, _ = attention_config.resolve_with_source(
|
||||||
|
role=role,
|
||||||
|
role_category=role_category,
|
||||||
|
)
|
||||||
|
if spec is not None:
|
||||||
|
from vllm_omni.diffusion.attention.backends.registry import DiffusionAttentionBackendEnum
|
||||||
|
|
||||||
|
return DiffusionAttentionBackendEnum[spec.backend.upper()].get_class()
|
||||||
|
return _cached_get_backend_cls(None, HEAD_SIZE_UNKNOWN)
|
||||||
+21
-17
@@ -7,7 +7,7 @@ import os
|
|||||||
import random
|
import random
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from dataclasses import dataclass, field, fields
|
from dataclasses import dataclass, field, fields
|
||||||
from enum import Enum, StrEnum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import diffusers
|
import diffusers
|
||||||
@@ -121,6 +121,8 @@ def validate_dlo_host_registration_options(
|
|||||||
hwr_mode: object,
|
hwr_mode: object,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Validate the optional transport budget without probing CUDA or HWR."""
|
"""Validate the optional transport budget without probing CUDA or HWR."""
|
||||||
|
if not isinstance(limit_gib, (int, float, str)):
|
||||||
|
raise TypeError(f"dlo_host_registration_limit_gib must be a number; got {type(limit_gib).__name__}")
|
||||||
value = float(limit_gib)
|
value = float(limit_gib)
|
||||||
if not math.isfinite(value) or value < 0:
|
if not math.isfinite(value) or value < 0:
|
||||||
raise ValueError("dlo_host_registration_limit_gib must be finite and >= 0")
|
raise ValueError("dlo_host_registration_limit_gib must be finite and >= 0")
|
||||||
@@ -1682,7 +1684,8 @@ class AttnQuantSpec:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
return self.dtype_qk is not None or self.dtype_vo is not None
|
# Include flashinfer_backend so a variant pin without dtypes is serialized.
|
||||||
|
return self.dtype_qk is not None or self.dtype_vo is not None or self.flashinfer_backend is not None
|
||||||
|
|
||||||
|
|
||||||
# Backends that select key blocks instead of attending densely, and so accept
|
# Backends that select key blocks instead of attending densely, and so accept
|
||||||
@@ -1690,7 +1693,7 @@ class AttnQuantSpec:
|
|||||||
BLOCK_SPARSE_BACKENDS = frozenset({"RAINFUSION_ATTN"})
|
BLOCK_SPARSE_BACKENDS = frozenset({"RAINFUSION_ATTN"})
|
||||||
|
|
||||||
|
|
||||||
class RainFusionPrecision(StrEnum):
|
class RainFusionPrecision(str, Enum):
|
||||||
"""Execution precision for block-sparse RainFusion (rf_v3) attention.
|
"""Execution precision for block-sparse RainFusion (rf_v3) attention.
|
||||||
|
|
||||||
``bf16``: no quantization, pure BF16 sparse attention (official baseline).
|
``bf16``: no quantization, pure BF16 sparse attention (official baseline).
|
||||||
@@ -1728,11 +1731,11 @@ class BlockSparseSpec:
|
|||||||
raise ValueError(f"block_sparse.start_step must be >= 0; got {self.start_step!r}.")
|
raise ValueError(f"block_sparse.start_step must be >= 0; got {self.start_step!r}.")
|
||||||
if self.end_step < 0:
|
if self.end_step < 0:
|
||||||
raise ValueError(f"block_sparse.end_step must be >= 0; got {self.end_step!r}.")
|
raise ValueError(f"block_sparse.end_step must be >= 0; got {self.end_step!r}.")
|
||||||
if self.precision not in {p.value for p in RainFusionPrecision}:
|
precision = self.precision.value if isinstance(self.precision, RainFusionPrecision) else self.precision
|
||||||
raise ValueError(
|
valid = {member.value for member in RainFusionPrecision}
|
||||||
f"block_sparse.precision must be one of {sorted(p.value for p in RainFusionPrecision)}; "
|
if precision not in valid:
|
||||||
f"got {self.precision!r}."
|
raise ValueError(f"block_sparse.precision must be one of {sorted(valid)}; got {self.precision!r}.")
|
||||||
)
|
self.precision = precision
|
||||||
self.skip_layer_indices = parse_kv_cache_skip_selector(self.skip_layers)
|
self.skip_layer_indices = parse_kv_cache_skip_selector(self.skip_layers)
|
||||||
|
|
||||||
|
|
||||||
@@ -1797,18 +1800,19 @@ class AttentionSpec:
|
|||||||
kw["target_sparsity"] = ss.target_sparsity
|
kw["target_sparsity"] = ss.target_sparsity
|
||||||
if ss.disabled_until_timestep:
|
if ss.disabled_until_timestep:
|
||||||
kw["disabled_until_timestep"] = ss.disabled_until_timestep
|
kw["disabled_until_timestep"] = ss.disabled_until_timestep
|
||||||
if self.quant is not None and self.quant.enabled:
|
if self.quant is not None:
|
||||||
q = self.quant
|
q = self.quant
|
||||||
quant_kw: dict[str, Any] = {
|
quant_kw: dict[str, Any] = {}
|
||||||
"dtype_qk": q.dtype_qk,
|
if q.dtype_qk is not None or q.dtype_vo is not None:
|
||||||
"q_block_size": q.q_block_size,
|
quant_kw["dtype_qk"] = q.dtype_qk
|
||||||
"k_block_size": q.k_block_size,
|
quant_kw["q_block_size"] = q.q_block_size
|
||||||
}
|
quant_kw["k_block_size"] = q.k_block_size
|
||||||
if q.dtype_vo is not None:
|
if q.dtype_vo is not None:
|
||||||
quant_kw["dtype_vo"] = q.dtype_vo
|
quant_kw["dtype_vo"] = q.dtype_vo
|
||||||
if q.flashinfer_backend is not None:
|
if q.flashinfer_backend is not None:
|
||||||
quant_kw["flashinfer_backend"] = q.flashinfer_backend
|
quant_kw["flashinfer_backend"] = q.flashinfer_backend
|
||||||
kw["quant"] = quant_kw
|
if quant_kw:
|
||||||
|
kw["quant"] = quant_kw
|
||||||
if self.fastvideo_vsa_topk is not None:
|
if self.fastvideo_vsa_topk is not None:
|
||||||
kw["topk"] = self.fastvideo_vsa_topk
|
kw["topk"] = self.fastvideo_vsa_topk
|
||||||
if self.block_sparse is not None:
|
if self.block_sparse is not None:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project and the HuggingFace Team.
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project and the HuggingFace Team.
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
@@ -35,6 +36,7 @@ based on the plan specification.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -46,8 +48,10 @@ from vllm_omni.diffusion.distributed.sp_plan import (
|
|||||||
AnySequenceParallelInput,
|
AnySequenceParallelInput,
|
||||||
SequenceParallelConfig,
|
SequenceParallelConfig,
|
||||||
SequenceParallelInput,
|
SequenceParallelInput,
|
||||||
|
SequenceParallelInputType,
|
||||||
SequenceParallelModelPlan,
|
SequenceParallelModelPlan,
|
||||||
SequenceParallelOutput,
|
SequenceParallelOutput,
|
||||||
|
SequenceParallelOutputType,
|
||||||
SequenceParallelPartialInput,
|
SequenceParallelPartialInput,
|
||||||
)
|
)
|
||||||
from vllm_omni.diffusion.distributed.sp_sharding import sp_gather, sp_shard
|
from vllm_omni.diffusion.distributed.sp_sharding import sp_gather, sp_shard
|
||||||
@@ -69,18 +73,18 @@ class ModuleForwardMetadata:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
cached_parameter_indices: dict[str, int] | None = None
|
cached_parameter_indices: dict[str, int] | None = None
|
||||||
_cls: type | None = None
|
_cls: type[nn.Module] | None = None
|
||||||
|
|
||||||
def _get_parameter_from_args_kwargs(
|
def _get_parameter_from_args_kwargs(
|
||||||
self,
|
self,
|
||||||
identifier: str,
|
identifier: str | int,
|
||||||
args: tuple = (),
|
args: Sequence[Any] = (),
|
||||||
kwargs: dict | None = None,
|
kwargs: dict[str, Any] | None = None,
|
||||||
) -> tuple[Any, bool, int | None]:
|
) -> tuple[Any, bool, int | None]:
|
||||||
"""Get a parameter value from args or kwargs by name.
|
"""Get a parameter value from args or kwargs by name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
identifier: The parameter name to look up.
|
identifier: The parameter name to look up, or a positional index.
|
||||||
args: Positional arguments passed to forward.
|
args: Positional arguments passed to forward.
|
||||||
kwargs: Keyword arguments passed to forward.
|
kwargs: Keyword arguments passed to forward.
|
||||||
|
|
||||||
@@ -95,6 +99,11 @@ class ModuleForwardMetadata:
|
|||||||
"""
|
"""
|
||||||
kwargs = kwargs or {}
|
kwargs = kwargs or {}
|
||||||
|
|
||||||
|
if isinstance(identifier, int):
|
||||||
|
if identifier < len(args):
|
||||||
|
return args[identifier], False, identifier
|
||||||
|
return None, False, identifier
|
||||||
|
|
||||||
# First check kwargs
|
# First check kwargs
|
||||||
if identifier in kwargs:
|
if identifier in kwargs:
|
||||||
return kwargs[identifier], True, None
|
return kwargs[identifier], True, None
|
||||||
@@ -164,7 +173,7 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
metadata: dict[str | int, AnySequenceParallelInput | list[AnySequenceParallelInput]],
|
metadata: SequenceParallelInputType,
|
||||||
config: SequenceParallelConfig,
|
config: SequenceParallelConfig,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -191,6 +200,8 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Get the parameter value
|
# Get the parameter value
|
||||||
|
if self.module_forward_metadata is None:
|
||||||
|
raise RuntimeError("SequenceParallelSplitHook.initialize_hook must run before forward")
|
||||||
input_val, is_kwarg, index = self.module_forward_metadata._get_parameter_from_args_kwargs(
|
input_val, is_kwarg, index = self.module_forward_metadata._get_parameter_from_args_kwargs(
|
||||||
name, args_list, kwargs
|
name, args_list, kwargs
|
||||||
)
|
)
|
||||||
@@ -200,6 +211,10 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
|
|
||||||
# Shard the input
|
# Shard the input
|
||||||
if isinstance(input_val, torch.Tensor):
|
if isinstance(input_val, torch.Tensor):
|
||||||
|
if not isinstance(spm, (SequenceParallelInput, SequenceParallelPartialInput)):
|
||||||
|
raise ValueError(
|
||||||
|
f"Expected SequenceParallelInput for tensor parameter '{name}', got {type(spm).__name__}"
|
||||||
|
)
|
||||||
input_val = self._prepare_sp_input(input_val, spm, args_list, kwargs)
|
input_val = self._prepare_sp_input(input_val, spm, args_list, kwargs)
|
||||||
elif isinstance(input_val, (list, tuple)):
|
elif isinstance(input_val, (list, tuple)):
|
||||||
# Handle list/tuple of tensors with per-element config
|
# Handle list/tuple of tensors with per-element config
|
||||||
@@ -221,6 +236,8 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
|
|
||||||
# Update args or kwargs
|
# Update args or kwargs
|
||||||
if is_kwarg:
|
if is_kwarg:
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise TypeError(f"Keyword parameter name must be str, got {type(name).__name__}")
|
||||||
kwargs[name] = input_val
|
kwargs[name] = input_val
|
||||||
elif index is not None and index < len(args_list):
|
elif index is not None and index < len(args_list):
|
||||||
args_list[index] = input_val
|
args_list[index] = input_val
|
||||||
@@ -270,8 +287,8 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
def _resolve_text_len(
|
def _resolve_text_len(
|
||||||
self,
|
self,
|
||||||
sp_input: SequenceParallelPartialInput,
|
sp_input: SequenceParallelPartialInput,
|
||||||
args: tuple,
|
args: Sequence[Any],
|
||||||
kwargs: dict,
|
kwargs: dict[str, Any],
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Resolve text length from the source specification."""
|
"""Resolve text length from the source specification."""
|
||||||
source = sp_input.text_len_source
|
source = sp_input.text_len_source
|
||||||
@@ -285,6 +302,8 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
|
|
||||||
# Try to get from kwargs/args
|
# Try to get from kwargs/args
|
||||||
try:
|
try:
|
||||||
|
if self.module_forward_metadata is None:
|
||||||
|
raise RuntimeError("SequenceParallelSplitHook.initialize_hook must run before forward")
|
||||||
val, _, _ = self.module_forward_metadata._get_parameter_from_args_kwargs(source, args, kwargs)
|
val, _, _ = self.module_forward_metadata._get_parameter_from_args_kwargs(source, args, kwargs)
|
||||||
if val is None:
|
if val is None:
|
||||||
raise ValueError(f"Parameter '{source}' is None, cannot determine text length.")
|
raise ValueError(f"Parameter '{source}' is None, cannot determine text length.")
|
||||||
@@ -306,8 +325,8 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
self,
|
self,
|
||||||
x: torch.Tensor,
|
x: torch.Tensor,
|
||||||
sp_input: AnySequenceParallelInput,
|
sp_input: AnySequenceParallelInput,
|
||||||
args: tuple = (),
|
args: Sequence[Any] = (),
|
||||||
kwargs: dict | None = None,
|
kwargs: dict[str, Any] | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""Shard a tensor according to the input specification."""
|
"""Shard a tensor according to the input specification."""
|
||||||
kwargs = kwargs or {}
|
kwargs = kwargs or {}
|
||||||
@@ -382,7 +401,7 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
2. Creates an attention mask indicating valid vs padding positions
|
2. Creates an attention mask indicating valid vs padding positions
|
||||||
3. Stores the mask and padding info in ForwardContext
|
3. Stores the mask and padding info in ForwardContext
|
||||||
"""
|
"""
|
||||||
from vllm_omni.diffusion.attention.selector import get_attn_backend_for_role
|
from vllm_omni.diffusion.attention.selector import get_attn_backend_for_capability
|
||||||
from vllm_omni.diffusion.distributed.parallel_state import (
|
from vllm_omni.diffusion.distributed.parallel_state import (
|
||||||
get_ring_parallel_world_size,
|
get_ring_parallel_world_size,
|
||||||
get_sequence_parallel_rank,
|
get_sequence_parallel_rank,
|
||||||
@@ -408,12 +427,14 @@ class SequenceParallelSplitHook(ModelHook):
|
|||||||
if od_config is not None:
|
if od_config is not None:
|
||||||
attention_config = od_config.diffusion_attention_config
|
attention_config = od_config.diffusion_attention_config
|
||||||
|
|
||||||
attn_backend, _ = get_attn_backend_for_role(
|
attn_backend = get_attn_backend_for_capability(
|
||||||
role="self",
|
role="self",
|
||||||
head_size=-1,
|
|
||||||
attention_config=attention_config,
|
attention_config=attention_config,
|
||||||
)
|
)
|
||||||
if not attn_backend.supports_attention_mask:
|
attention_spec = None
|
||||||
|
if attention_config is not None:
|
||||||
|
attention_spec, _ = attention_config.resolve_with_source(role="self")
|
||||||
|
if not attn_backend.supports_attention_mask(attention_spec):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Sequence length ({seq_len}) is not divisible by SP world size ({world_size}). "
|
f"Sequence length ({seq_len}) is not divisible by SP world size ({world_size}). "
|
||||||
f"Cannot use {attn_backend.get_name()} which does not support attention_mask. "
|
f"Cannot use {attn_backend.get_name()} which does not support attention_mask. "
|
||||||
@@ -468,12 +489,14 @@ class SequenceParallelGatherHook(ModelHook):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
metadata: SequenceParallelOutput | list[SequenceParallelOutput],
|
metadata: SequenceParallelOutputType,
|
||||||
config: SequenceParallelConfig,
|
config: SequenceParallelConfig,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if isinstance(metadata, SequenceParallelOutput):
|
if isinstance(metadata, SequenceParallelOutput):
|
||||||
metadata = [metadata]
|
metadata = [metadata]
|
||||||
|
elif isinstance(metadata, tuple):
|
||||||
|
metadata = list(metadata)
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
@@ -630,6 +653,7 @@ def apply_sequence_parallel(
|
|||||||
logger.debug(f"Applying SP hooks to '{module_id}' ({len(submodule)} module(s))")
|
logger.debug(f"Applying SP hooks to '{module_id}' ({len(submodule)} module(s))")
|
||||||
|
|
||||||
for m in submodule:
|
for m in submodule:
|
||||||
|
hook: SequenceParallelSplitHook | SequenceParallelGatherHook
|
||||||
if isinstance(sp_model_plan, dict):
|
if isinstance(sp_model_plan, dict):
|
||||||
# Input specification
|
# Input specification
|
||||||
hook = SequenceParallelSplitHook(sp_model_plan, config)
|
hook = SequenceParallelSplitHook(sp_model_plan, config)
|
||||||
@@ -638,6 +662,8 @@ def apply_sequence_parallel(
|
|||||||
# Output specification
|
# Output specification
|
||||||
if isinstance(sp_model_plan, SequenceParallelOutput):
|
if isinstance(sp_model_plan, SequenceParallelOutput):
|
||||||
sp_model_plan = [sp_model_plan]
|
sp_model_plan = [sp_model_plan]
|
||||||
|
elif isinstance(sp_model_plan, tuple):
|
||||||
|
sp_model_plan = list(sp_model_plan)
|
||||||
if not all(isinstance(x, SequenceParallelOutput) or x is None for x in sp_model_plan):
|
if not all(isinstance(x, SequenceParallelOutput) or x is None for x in sp_model_plan):
|
||||||
raise ValueError(f"Expected SequenceParallelOutput elements, got {sp_model_plan}")
|
raise ValueError(f"Expected SequenceParallelOutput elements, got {sp_model_plan}")
|
||||||
hook = SequenceParallelGatherHook(sp_model_plan, config)
|
hook = SequenceParallelGatherHook(sp_model_plan, config)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""Diffusers backend adapter for vLLM-Omni.
|
"""Diffusers backend adapter for vLLM-Omni.
|
||||||
|
|
||||||
Provides a black-box wrapper around any 🤗 Diffusers pipeline, enabling
|
Provides a black-box wrapper around any 🤗 Diffusers pipeline, enabling
|
||||||
@@ -44,13 +44,21 @@ logger = logging.getLogger(__name__)
|
|||||||
# with the accuracy tests' diffusers reference runs so both sides of a
|
# with the accuracy tests' diffusers reference runs so both sides of a
|
||||||
# similarity comparison resolve to the same backend on any given image
|
# similarity comparison resolve to the same backend on any given image
|
||||||
# (hub kernels may lack a build variant for the image's torch).
|
# (hub kernels may lack a build variant for the image's torch).
|
||||||
CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS: list[str] = [
|
# Automatic / FLASH_ATTN selection walks the full chain. Explicit Hub
|
||||||
|
# options attempt only their matching Diffusers backend names.
|
||||||
|
CUDA_FLASH_ATTN_3_HUB_ATTEMPTS: list[str] = [
|
||||||
"_flash_3_hub",
|
"_flash_3_hub",
|
||||||
"_flash_3_varlen_hub",
|
"_flash_3_varlen_hub",
|
||||||
"_flash_3",
|
]
|
||||||
"_flash_varlen_3",
|
CUDA_FLASH_ATTN_HUB_ATTEMPTS: list[str] = [
|
||||||
"flash_hub",
|
"flash_hub",
|
||||||
"flash_varlen_hub",
|
"flash_varlen_hub",
|
||||||
|
]
|
||||||
|
CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS: list[str] = [
|
||||||
|
*CUDA_FLASH_ATTN_3_HUB_ATTEMPTS,
|
||||||
|
"_flash_3",
|
||||||
|
"_flash_varlen_3",
|
||||||
|
*CUDA_FLASH_ATTN_HUB_ATTEMPTS,
|
||||||
"flash",
|
"flash",
|
||||||
"flash_varlen",
|
"flash_varlen",
|
||||||
"_native_flash",
|
"_native_flash",
|
||||||
@@ -293,6 +301,24 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
# Wrap settings, inputs, and outputs
|
# Wrap settings, inputs, and outputs
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_blackwell_gpu() -> bool:
|
||||||
|
"""Return True on Blackwell-class CUDA GPUs (sm_10x / sm_11x / sm_12x)."""
|
||||||
|
capability = None
|
||||||
|
try:
|
||||||
|
capability = current_omni_platform.get_device_capability()
|
||||||
|
except Exception:
|
||||||
|
capability = None
|
||||||
|
if capability is not None:
|
||||||
|
return capability.major in (10, 11, 12)
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
try:
|
||||||
|
major, _ = torch.cuda.get_device_capability()
|
||||||
|
return major in (10, 11, 12)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _set_attention_backend(self) -> None:
|
def _set_attention_backend(self) -> None:
|
||||||
"""Set the attention backend.
|
"""Set the attention backend.
|
||||||
|
|
||||||
@@ -308,6 +334,20 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
attention_backend_config = default_spec.backend if default_spec is not None else None
|
attention_backend_config = default_spec.backend if default_spec is not None else None
|
||||||
attention_backend_attempts: list[str] = []
|
attention_backend_attempts: list[str] = []
|
||||||
match attention_backend_config:
|
match attention_backend_config:
|
||||||
|
case "FLASH_ATTN_3_HUB":
|
||||||
|
if self._is_blackwell_gpu():
|
||||||
|
raise ValueError(
|
||||||
|
"FLASH_ATTN_3_HUB was explicitly selected for a Diffusers adapter on "
|
||||||
|
"Blackwell, but no runnable sm_10x+ Flash Attention kernel is available."
|
||||||
|
)
|
||||||
|
attention_backend_attempts.extend(CUDA_FLASH_ATTN_3_HUB_ATTEMPTS)
|
||||||
|
case "FLASH_ATTN_HUB":
|
||||||
|
if self._is_blackwell_gpu():
|
||||||
|
raise ValueError(
|
||||||
|
"FLASH_ATTN_HUB was explicitly selected for a Diffusers adapter on "
|
||||||
|
"Blackwell, but no runnable sm_10x+ Flash Attention kernel is available."
|
||||||
|
)
|
||||||
|
attention_backend_attempts.extend(CUDA_FLASH_ATTN_HUB_ATTEMPTS)
|
||||||
case "FLASH_ATTN" | None:
|
case "FLASH_ATTN" | None:
|
||||||
if current_omni_platform.is_rocm():
|
if current_omni_platform.is_rocm():
|
||||||
attention_backend_attempts.append("aiter")
|
attention_backend_attempts.append("aiter")
|
||||||
@@ -318,17 +358,31 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
"Unknown diffusers attention backend option for MUSA platform. Falling back to SDPA."
|
"Unknown diffusers attention backend option for MUSA platform. Falling back to SDPA."
|
||||||
)
|
)
|
||||||
attention_backend_attempts.append("native")
|
attention_backend_attempts.append("native")
|
||||||
|
elif self._is_blackwell_gpu():
|
||||||
|
# Installed FA2/FA3 (and HF FA3 hub) wheels are typically Hopper-only.
|
||||||
|
# set_attention_backend("_flash_3*") can succeed on import, then crash
|
||||||
|
# at runtime with "no kernel image is available for execution on the device".
|
||||||
|
if attention_backend_config is not None:
|
||||||
|
raise ValueError(
|
||||||
|
f"{attention_backend_config} was explicitly selected for a Diffusers adapter on "
|
||||||
|
"Blackwell, but no runnable sm_10x+ Flash Attention kernel is available."
|
||||||
|
)
|
||||||
|
attention_backend_attempts.append("native")
|
||||||
else:
|
else:
|
||||||
attention_backend_attempts.extend(CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS)
|
attention_backend_attempts.extend(CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS)
|
||||||
case "SAGE_ATTN":
|
case "SAGE_ATTN":
|
||||||
attention_backend_attempts.extend(["sage_hub", "sage", "sage", "sage_varlen"])
|
attention_backend_attempts.extend(["sage_hub", "sage", "sage_varlen"])
|
||||||
case "ASCEND":
|
case "ASCEND":
|
||||||
attention_backend_attempts.append("_native_npu")
|
attention_backend_attempts.append("_native_npu")
|
||||||
case "TORCH_SDPA":
|
case "TORCH_SDPA":
|
||||||
attention_backend_attempts.append("native")
|
attention_backend_attempts.append("native")
|
||||||
|
case "CUDNN_ATTN" | "FLASHINFER_ATTN" | "TRTLLM_ATTN":
|
||||||
|
raise ValueError(
|
||||||
|
f"{attention_backend_config} was explicitly selected, but Diffusers adapters do not "
|
||||||
|
"provide a binding for this vLLM-Omni backend. Select TORCH_SDPA or a Diffusers-native backend."
|
||||||
|
)
|
||||||
case _:
|
case _:
|
||||||
logger.warning(f"Invalid attention backend: {attention_backend_config}. Falling back to SDPA.")
|
raise ValueError(f"Invalid Diffusers attention backend: {attention_backend_config}.")
|
||||||
attention_backend_attempts.append("native")
|
|
||||||
|
|
||||||
attempt_errors: list[str] = []
|
attempt_errors: list[str] = []
|
||||||
set_backend: str | None = None
|
set_backend: str | None = None
|
||||||
@@ -340,8 +394,15 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
attempt_errors.append(str(e))
|
attempt_errors.append(str(e))
|
||||||
|
|
||||||
# If all attempts fail, fallback to SDPA and warn the user about the failures
|
# Automatic selection may use native SDPA as its final safe choice. An
|
||||||
|
# explicitly configured backend must either be installed or raise.
|
||||||
if len(attempt_errors) == len(attention_backend_attempts):
|
if len(attempt_errors) == len(attention_backend_attempts):
|
||||||
|
if attention_backend_config is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to set explicitly selected attention backend '{attention_backend_config}' for "
|
||||||
|
f"diffusers pipeline {self._pipeline.__class__.__name__}. Attempts: "
|
||||||
|
f"{dict(zip(attention_backend_attempts, attempt_errors))}"
|
||||||
|
)
|
||||||
self._pipeline.transformer.set_attention_backend("native")
|
self._pipeline.transformer.set_attention_backend("native")
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to set attention backend '{attention_backend_config}' for "
|
f"Failed to set attention backend '{attention_backend_config}' for "
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
@@ -52,6 +52,21 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def has_flash_attn_4(cls) -> bool:
|
||||||
|
"""Return whether CuTe FA4 is importable (Blackwell-capable FLASH_ATTN)."""
|
||||||
|
from vllm_omni.diffusion.attention.backends.utils.fa import is_flash_attn_4_available
|
||||||
|
|
||||||
|
return is_flash_attn_4_available()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def supports_diffusion_dense_flash_attention(cls) -> bool:
|
||||||
|
"""Dense FLASH_ATTN is FA4 on Blackwell; FA2/FA3 on older CUDA GPUs."""
|
||||||
|
capability = cls.get_device_capability()
|
||||||
|
if capability is not None and capability.major in (10, 11, 12):
|
||||||
|
return cls.has_flash_attn_4()
|
||||||
|
return cls.has_flash_attn_package()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_diffusion_attn_backend_cls(
|
def get_diffusion_attn_backend_cls(
|
||||||
cls,
|
cls,
|
||||||
@@ -84,7 +99,7 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
|
|||||||
is_blackwell = major in (10, 11, 12)
|
is_blackwell = major in (10, 11, 12)
|
||||||
if is_blackwell and (major, minor) not in _known_blackwell_sms:
|
if is_blackwell and (major, minor) not in _known_blackwell_sms:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Detected Blackwell-class GPU %s (untested variant); routing to CUDNN_ATTN with SDPA fallback.",
|
"Detected Blackwell-class GPU %s (untested variant); routing to CUDNN_ATTN.",
|
||||||
sm_str,
|
sm_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,19 +107,24 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
|
|||||||
packages_info = PACKAGES_CHECKER.get_packages_info()
|
packages_info = PACKAGES_CHECKER.get_packages_info()
|
||||||
packages_available = packages_info.get("has_flash_attn", False)
|
packages_available = packages_info.get("has_flash_attn", False)
|
||||||
|
|
||||||
# Both compute capability and packages must be available for FA
|
# FA2/FA3 wheels are often importable on Blackwell but only ship
|
||||||
flash_attn_supported = compute_supported and packages_available
|
# Ampere/Ada/Hopper kernels; the first forward then dies with
|
||||||
|
# "no kernel image". FA4 (flash_attn.cute / vllm-omni[fa4]) is the
|
||||||
|
# FLASH_ATTN implementation that actually runs on Blackwell.
|
||||||
|
if is_blackwell:
|
||||||
|
flash_attn_supported = compute_supported and cls.has_flash_attn_4()
|
||||||
|
else:
|
||||||
|
flash_attn_supported = compute_supported and packages_available
|
||||||
|
|
||||||
# cuDNN 9.5+ ships Blackwell FMHA kernels. If the runtime is older,
|
# cuDNN 9.5+ ships Blackwell FMHA kernels. If the runtime is older,
|
||||||
# the CUDNN_ATTN default would still work via internal fallback but
|
# skip the CUDNN_ATTN default rather than selecting a backend whose
|
||||||
# without the tuned Blackwell path, so we skip routing there.
|
# kernel selector may reject the runtime shape.
|
||||||
cudnn_version = torch.backends.cudnn.version() or 0
|
cudnn_version = torch.backends.cudnn.version() or 0
|
||||||
cudnn_blackwell_ready = cudnn_version >= 90500
|
cudnn_blackwell_ready = cudnn_version >= 90500
|
||||||
|
|
||||||
# FlashInfer edges cuDNN by ~4% at the kernel level on sm_120 but
|
# FlashInfer edges cuDNN by ~4% at the kernel level on sm_120 but
|
||||||
# regresses ~2x at e2e on HV-1.5 because its dense-prefill path can't
|
# regresses ~2x at e2e on HV-1.5 because its dense-prefill path cannot
|
||||||
# take 2D attn_masks and the SDPA fallback dispatches to
|
# represent every diffusion attention mask without changing semantics.
|
||||||
# EFFICIENT_ATTENTION (~25 ms) instead of the cuDNN mask path (~11 ms).
|
|
||||||
# CUDNN_ATTN pins sdpa_kernel([CUDNN_ATTENTION]) directly so masked
|
# CUDNN_ATTN pins sdpa_kernel([CUDNN_ATTENTION]) directly so masked
|
||||||
# calls keep the cuDNN path. Blackwell default prefers CUDNN_ATTN;
|
# calls keep the cuDNN path. Blackwell default prefers CUDNN_ATTN;
|
||||||
# users can opt into FLASHINFER_ATTN explicitly for no-mask workloads.
|
# users can opt into FLASHINFER_ATTN explicitly for no-mask workloads.
|
||||||
@@ -120,6 +140,18 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
|
|||||||
# not abort startup — just treat FlashInfer as unavailable.
|
# not abort startup — just treat FlashInfer as unavailable.
|
||||||
logger.debug("FlashInfer probe failed (%s); treating as unavailable", e)
|
logger.debug("FlashInfer probe failed (%s); treating as unavailable", e)
|
||||||
|
|
||||||
|
# FLASHINFER_ATTN needs BatchPrefillWithRaggedKVCacheWrapper, not just a
|
||||||
|
# top-level flashinfer package. Probe the actual symbol so a stub /
|
||||||
|
# partial wheel is not auto-routed then crash at layer init.
|
||||||
|
flashinfer_prefill_available = False
|
||||||
|
if flashinfer_available:
|
||||||
|
try:
|
||||||
|
from flashinfer.prefill import BatchPrefillWithRaggedKVCacheWrapper # noqa: F401
|
||||||
|
|
||||||
|
flashinfer_prefill_available = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("FlashInfer prefill wrapper probe failed (%s); treating as unavailable", e)
|
||||||
|
|
||||||
# TRTLLM_ATTN needs the trtllm-gen kernel specifically, not just any FlashInfer
|
# TRTLLM_ATTN needs the trtllm-gen kernel specifically, not just any FlashInfer
|
||||||
# wheel. Probe the actual symbol so a released wheel lacking it does not get
|
# wheel. Probe the actual symbol so a released wheel lacking it does not get
|
||||||
# auto-routed to TRTLLM_ATTN and then crash on the first forward.
|
# auto-routed to TRTLLM_ATTN and then crash on the first forward.
|
||||||
@@ -134,58 +166,117 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
|
|||||||
|
|
||||||
if selected_backend is not None:
|
if selected_backend is not None:
|
||||||
backend_upper = selected_backend.upper()
|
backend_upper = selected_backend.upper()
|
||||||
|
# Architecture / package probes for optional kernels must run before
|
||||||
|
# validate_diffusion_attn_backend, which imports the backend module
|
||||||
|
# and would otherwise load sageattention / sageattn3 on unsupported GPUs.
|
||||||
|
if backend_upper == "SAGE_ATTN":
|
||||||
|
sage_supported_sms = {(8, 0), (8, 6), (8, 9), (9, 0), (12, 0), (12, 1)}
|
||||||
|
if compute_capability is None or tuple(compute_capability) not in sage_supported_sms:
|
||||||
|
raise ValueError(
|
||||||
|
f"SAGE_ATTN was explicitly selected but does not provide a kernel for {sm_str or 'this GPU'}. "
|
||||||
|
"Select a compatible backend."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
importlib.import_module("sageattention")
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"SAGE_ATTN was explicitly selected, but the sageattention package is not available."
|
||||||
|
) from e
|
||||||
|
if backend_upper == "SAGE_ATTN_3":
|
||||||
|
sage_attn3_supported = compute_capability is not None and compute_capability.major >= 10
|
||||||
|
if not sage_attn3_supported:
|
||||||
|
raise ValueError(
|
||||||
|
"SAGE_ATTN_3 was explicitly selected but requires a Blackwell-class GPU "
|
||||||
|
"with compute capability >= 10.0. Select a compatible backend."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
importlib.import_module("sageattn3")
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"SAGE_ATTN_3 was explicitly selected, but the sageattn3 package is not available. "
|
||||||
|
"Install SageAttention/sageattention3_blackwell or select a different backend."
|
||||||
|
) from e
|
||||||
cls.validate_diffusion_attn_backend(backend_upper)
|
cls.validate_diffusion_attn_backend(backend_upper)
|
||||||
|
if backend_upper == "SAGE_ATTN_3" and head_size > 0:
|
||||||
|
sage3_cls = DiffusionAttentionBackendEnum.SAGE_ATTN_3.get_class()
|
||||||
|
if not sage3_cls.supports_head_size(head_size):
|
||||||
|
raise ValueError(
|
||||||
|
f"SAGE_ATTN_3 was explicitly selected but head_size={head_size} is unsupported. "
|
||||||
|
f"Supported head sizes: {sage3_cls.get_supported_head_sizes()}. "
|
||||||
|
"Select TORCH_SDPA or another backend."
|
||||||
|
)
|
||||||
if backend_upper in ("FLASH_ATTN_HUB", "FLASH_ATTN_3_HUB"):
|
if backend_upper in ("FLASH_ATTN_HUB", "FLASH_ATTN_3_HUB"):
|
||||||
try:
|
try:
|
||||||
importlib.import_module("kernels")
|
importlib.import_module("kernels")
|
||||||
logger.info("Using HuggingFace kernels-backed attention backend '%s'", backend_upper)
|
logger.info("Using HuggingFace kernels-backed attention backend '%s'", backend_upper)
|
||||||
except ImportError:
|
except ImportError as e:
|
||||||
if backend_upper == "FLASH_ATTN_HUB":
|
raise ImportError(
|
||||||
logger.warning(
|
f"{backend_upper} was explicitly selected, but the HuggingFace `kernels` "
|
||||||
"HuggingFace `kernels` library is not available. Falling back to local FLASH_ATTN."
|
"library is not available. Install `kernels` or select a different backend."
|
||||||
)
|
) from e
|
||||||
backend_upper = "FLASH_ATTN"
|
|
||||||
elif backend_upper == "FLASH_ATTN_3_HUB":
|
if backend_upper == "FLASH_ATTN_HUB":
|
||||||
logger.warning(
|
fa2_hub_supported = compute_capability is not None and compute_capability.major in (8, 9)
|
||||||
"HuggingFace `kernels` library is not available. Falling back to local FLASH_ATTN."
|
if not fa2_hub_supported:
|
||||||
)
|
raise ValueError(
|
||||||
backend_upper = "FLASH_ATTN"
|
"FLASH_ATTN_HUB was explicitly selected but its current FA2 kernels require "
|
||||||
|
"an Ampere, Ada, or Hopper GPU (compute capability 8.x/9.x)."
|
||||||
|
)
|
||||||
|
|
||||||
if backend_upper == "FLASH_ATTN_3_HUB":
|
if backend_upper == "FLASH_ATTN_3_HUB":
|
||||||
fa3_hub_supported = compute_capability is not None and compute_capability.major >= 9
|
fa3_hub_supported = compute_capability is not None and compute_capability.major == 9
|
||||||
if not fa3_hub_supported:
|
if not fa3_hub_supported:
|
||||||
logger.warning(
|
raise ValueError(
|
||||||
"FLASH_ATTN_3_HUB requires a Hopper-class GPU with compute capability >= 9.0. "
|
"FLASH_ATTN_3_HUB was explicitly selected but its current kernels require a Hopper GPU "
|
||||||
"Falling back to FLASH_ATTN_HUB."
|
"with compute capability 9.x. Select a compatible backend."
|
||||||
)
|
)
|
||||||
backend_upper = "FLASH_ATTN_HUB"
|
|
||||||
|
|
||||||
if backend_upper == "FLASH_ATTN" and not flash_attn_supported:
|
if backend_upper == "FLASH_ATTN" and not flash_attn_supported:
|
||||||
if not compute_supported:
|
if is_blackwell:
|
||||||
logger.warning(
|
reason = (
|
||||||
"Flash Attention requires GPU with compute capability >= 8.0. "
|
"Blackwell requires CuTe FlashAttention-4 "
|
||||||
"Falling back to TORCH_SDPA backend."
|
"(flash_attn.cute / vllm-omni[fa4]); FA2/FA3 kernels are Hopper-only"
|
||||||
)
|
)
|
||||||
elif not packages_available:
|
elif not compute_supported:
|
||||||
logger.warning("Flash Attention packages not available. Falling back to TORCH_SDPA backend.")
|
reason = "compute capability < 8.0"
|
||||||
logger.debug("Defaulting to diffusion attention backend SDPA")
|
else:
|
||||||
return DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
reason = "Flash Attention package unavailable"
|
||||||
if backend_upper == "SAGE_ATTN_3":
|
raise ValueError(
|
||||||
sage_attn3_supported = compute_capability is not None and compute_capability.major >= 10
|
f"FLASH_ATTN was explicitly selected but is unsupported ({reason}). Select a compatible backend."
|
||||||
if not sage_attn3_supported:
|
)
|
||||||
logger.warning(
|
if backend_upper == "FLASHINFER_ATTN" and not flashinfer_available:
|
||||||
"SageAttention3 requires a Blackwell-class GPU with compute capability >= 10.0. "
|
raise ValueError(
|
||||||
"Falling back to TORCH_SDPA backend."
|
"FLASHINFER_ATTN was explicitly selected, but FlashInfer is unavailable. "
|
||||||
|
"Install a compatible FlashInfer build or select a different backend."
|
||||||
|
)
|
||||||
|
if backend_upper == "FLASHINFER_ATTN" and not flashinfer_prefill_available:
|
||||||
|
raise ValueError(
|
||||||
|
"FLASHINFER_ATTN was explicitly selected, but the installed FlashInfer build does not "
|
||||||
|
"provide BatchPrefillWithRaggedKVCacheWrapper. Install a compatible build."
|
||||||
|
)
|
||||||
|
if backend_upper == "FLASHINFER_ATTN":
|
||||||
|
from vllm_omni.diffusion.attention.backends.flashinfer_attn import FlashInferAttentionBackend
|
||||||
|
|
||||||
|
# head_size <= 0 is the capability-probe sentinel. Skip geometry
|
||||||
|
# validation; Attention construction still checks the real size.
|
||||||
|
if head_size > 0 and not FlashInferAttentionBackend.supports_head_size(head_size):
|
||||||
|
raise ValueError(
|
||||||
|
f"FLASHINFER_ATTN was explicitly selected but head_size={head_size} is unsupported. "
|
||||||
|
f"Supported head sizes: {FlashInferAttentionBackend.get_supported_head_sizes()}. "
|
||||||
|
"Select TORCH_SDPA or another backend."
|
||||||
)
|
)
|
||||||
return DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
if backend_upper == "CUDNN_ATTN":
|
||||||
try:
|
from vllm_omni.diffusion.attention.backends.cudnn_attn import CuDNNAttentionBackend
|
||||||
importlib.import_module("sageattn3")
|
|
||||||
except ImportError:
|
# head_size <= 0 is the capability-probe sentinel (auto-pad does
|
||||||
logger.warning(
|
# not know head_dim). Skip geometry validation; the real
|
||||||
"SageAttention3 package not available. Install it from "
|
# Attention construction still checks the configured size.
|
||||||
"SageAttention/sageattention3_blackwell. Falling back to TORCH_SDPA backend."
|
if head_size > 0 and not CuDNNAttentionBackend.supports_head_size(head_size):
|
||||||
|
raise ValueError(
|
||||||
|
f"CUDNN_ATTN was explicitly selected but head_size={head_size} is unsupported. "
|
||||||
|
"Blackwell cuDNN FMHA requires head_dim divisible by 8 and no larger than 256. "
|
||||||
|
"Select FLASHINFER_ATTN or TORCH_SDPA."
|
||||||
)
|
)
|
||||||
return DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
|
|
||||||
if backend_upper == "TRTLLM_ATTN":
|
if backend_upper == "TRTLLM_ATTN":
|
||||||
trtllm_attn_supported = compute_capability is not None and compute_capability.major == 10
|
trtllm_attn_supported = compute_capability is not None and compute_capability.major == 10
|
||||||
if not trtllm_attn_supported:
|
if not trtllm_attn_supported:
|
||||||
@@ -194,9 +285,10 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
|
|||||||
"Blackwell GPU (SM100 / SM103, compute capability 10.x). Select a "
|
"Blackwell GPU (SM100 / SM103, compute capability 10.x). Select a "
|
||||||
"different --diffusion-attention-backend."
|
"different --diffusion-attention-backend."
|
||||||
)
|
)
|
||||||
if not flashinfer_available:
|
if not trtllm_gen_available:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"TRTLLM_ATTN diffusion attention backend requires flashinfer, which is not installed."
|
"TRTLLM_ATTN was explicitly selected, but the installed FlashInfer build does not "
|
||||||
|
"provide trtllm_ragged_attention_deepseek. Install a compatible build."
|
||||||
)
|
)
|
||||||
backend = DiffusionAttentionBackendEnum[backend_upper]
|
backend = DiffusionAttentionBackendEnum[backend_upper]
|
||||||
logger.debug("Using diffusion attention backend '%s'", backend_upper)
|
logger.debug("Using diffusion attention backend '%s'", backend_upper)
|
||||||
@@ -218,24 +310,40 @@ class CudaOmniPlatform(OmniPlatform, CudaPlatformBase):
|
|||||||
return DiffusionAttentionBackendEnum.TRTLLM_ATTN.get_path()
|
return DiffusionAttentionBackendEnum.TRTLLM_ATTN.get_path()
|
||||||
|
|
||||||
if is_blackwell and cudnn_blackwell_ready:
|
if is_blackwell and cudnn_blackwell_ready:
|
||||||
logger.info(
|
from vllm_omni.diffusion.attention.backends.cudnn_attn import CuDNNAttentionBackend
|
||||||
"Defaulting to diffusion attention backend CUDNN_ATTN (Blackwell %s, cuDNN %d)",
|
|
||||||
sm_str,
|
|
||||||
cudnn_version,
|
|
||||||
)
|
|
||||||
return DiffusionAttentionBackendEnum.CUDNN_ATTN.get_path()
|
|
||||||
|
|
||||||
if is_blackwell and flashinfer_available:
|
# Unknown head_size is a capability probe (SP auto-pad), not a
|
||||||
|
# real DiT geometry. Keep CUDNN so mask support is reported.
|
||||||
|
if head_size <= 0 or CuDNNAttentionBackend.supports_head_size(head_size):
|
||||||
|
logger.info(
|
||||||
|
"Defaulting to diffusion attention backend CUDNN_ATTN (Blackwell %s, cuDNN %d, head_dim %d)",
|
||||||
|
sm_str,
|
||||||
|
cudnn_version,
|
||||||
|
head_size,
|
||||||
|
)
|
||||||
|
return DiffusionAttentionBackendEnum.CUDNN_ATTN.get_path()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Defaulting to diffusion attention backend FLASHINFER_ATTN (Blackwell %s, cuDNN unavailable)",
|
"Skipping CUDNN_ATTN on Blackwell %s: head_dim %d is outside cuDNN FMHA "
|
||||||
|
"(multiples of 8, 8-256). Automatic selection will use FlashInfer when available, otherwise SDPA.",
|
||||||
sm_str,
|
sm_str,
|
||||||
|
head_size,
|
||||||
)
|
)
|
||||||
return DiffusionAttentionBackendEnum.FLASHINFER_ATTN.get_path()
|
|
||||||
|
if is_blackwell and flashinfer_prefill_available:
|
||||||
|
from vllm_omni.diffusion.attention.backends.flashinfer_attn import FlashInferAttentionBackend
|
||||||
|
|
||||||
|
if FlashInferAttentionBackend.supports_head_size(head_size):
|
||||||
|
logger.info(
|
||||||
|
"Defaulting to diffusion attention backend FLASHINFER_ATTN (Blackwell %s, head_dim %d)",
|
||||||
|
sm_str,
|
||||||
|
head_size,
|
||||||
|
)
|
||||||
|
return DiffusionAttentionBackendEnum.FLASHINFER_ATTN.get_path()
|
||||||
|
|
||||||
if is_blackwell and not cudnn_blackwell_ready:
|
if is_blackwell and not cudnn_blackwell_ready:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Detected Blackwell %s but cuDNN %d < 9.5 — no tuned Blackwell FMHA. "
|
"Detected Blackwell %s but cuDNN %d < 9.5 — no tuned Blackwell FMHA. "
|
||||||
"Falling through to FLASH_ATTN / SDPA.",
|
"Automatic backend selection will use FlashInfer when available, otherwise SDPA.",
|
||||||
sm_str,
|
sm_str,
|
||||||
cudnn_version,
|
cudnn_version,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user