Files
hao-ai-lab-FastVideo/fastvideo/platforms/cuda.py
T

496 lines
22 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/platforms/cuda.py
"""Code inside this file can safely assume cuda platform, e.g. importing
pynvml. However, it should not initialize cuda context.
"""
import ctypes
import os
from collections.abc import Callable
from functools import lru_cache, wraps
from typing import TypeVar
import torch
from typing_extensions import ParamSpec
import fastvideo.envs as envs
from fastvideo.logger import init_logger
from fastvideo.platforms.interface import (AttentionBackendEnum, DeviceCapability, Platform, PlatformEnum)
from fastvideo.utils import import_pynvml
logger = init_logger(__name__)
_P = ParamSpec("_P")
_R = TypeVar("_R")
pynvml = import_pynvml() # type: ignore[no-untyped-call]
_CUDA_SUCCESS = 0
# Stable value from CUDA's public CUdevice_attribute enum.
_CU_DEVICE_ATTRIBUTE_INTEGRATED = 18
_CUDA_DRIVER_LIBRARY = "nvcuda.dll" if os.name == "nt" else "libcuda.so.1"
def _cuda_driver_device_is_integrated(device_id: int) -> bool:
"""Query ``CU_DEVICE_ATTRIBUTE_INTEGRATED`` without creating a context.
``cuInit`` loads the CUDA driver, while these device-management calls only
inspect the logical device ordinal. They neither create nor retain a CUDA
context. Driver initialization is not pre-fork safe, so this probe must
remain worker-local after process creation and device binding. Using the
driver ordinal preserves CUDA_VISIBLE_DEVICES ordering, including UUID and
MIG selectors.
"""
try:
driver = ctypes.CDLL(_CUDA_DRIVER_LIBRARY)
driver.cuInit.argtypes = [ctypes.c_uint]
driver.cuInit.restype = ctypes.c_int
driver.cuDeviceGet.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
driver.cuDeviceGet.restype = ctypes.c_int
driver.cuDeviceGetAttribute.argtypes = [
ctypes.POINTER(ctypes.c_int),
ctypes.c_int,
ctypes.c_int,
]
driver.cuDeviceGetAttribute.restype = ctypes.c_int
if driver.cuInit(0) != _CUDA_SUCCESS:
return False
device = ctypes.c_int()
if driver.cuDeviceGet(ctypes.byref(device), device_id) != _CUDA_SUCCESS:
return False
is_integrated = ctypes.c_int()
if driver.cuDeviceGetAttribute(
ctypes.byref(is_integrated),
_CU_DEVICE_ATTRIBUTE_INTEGRATED,
device,
) != _CUDA_SUCCESS:
return False
return bool(is_integrated.value)
except Exception:
# Missing/incompatible driver libraries and unavailable devices must
# preserve the established discrete-memory offload policy.
return False
# pytorch 2.5 uses cudnn sdpa by default, which will cause crash on some models
# see https://github.com/huggingface/diffusers/issues/9704 for details
torch.backends.cuda.enable_cudnn_sdp(False)
def device_id_to_physical_device_id(device_id: int) -> int:
if "CUDA_VISIBLE_DEVICES" in os.environ:
device_ids = os.environ["CUDA_VISIBLE_DEVICES"].split(",")
if device_ids == [""]:
msg = ("CUDA_VISIBLE_DEVICES is set to empty string, which means"
" GPU support is disabled. If you are using ray, please unset"
" the environment variable `CUDA_VISIBLE_DEVICES` inside the"
" worker/actor. "
"Check https://github.com/vllm-project/vllm/issues/8402 for"
" more information.")
raise RuntimeError(msg)
physical_device_id = device_ids[device_id]
return int(physical_device_id)
else:
return device_id
def with_nvml_context(fn: Callable[_P, _R]) -> Callable[_P, _R]:
@wraps(fn)
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
pynvml.nvmlInit()
try:
return fn(*args, **kwargs)
finally:
pynvml.nvmlShutdown()
return wrapper
class CudaPlatformBase(Platform):
_enum = PlatformEnum.CUDA
device_name: str = "cuda"
device_type: str = "cuda"
dispatch_key: str = "CUDA"
ray_device_key: str = "GPU"
device_control_env_var: str = "CUDA_VISIBLE_DEVICES"
@classmethod
def get_device_capability(cls, device_id: int = 0) -> DeviceCapability | None:
raise NotImplementedError
@classmethod
def get_device_name(cls, device_id: int = 0) -> str:
raise NotImplementedError
@classmethod
def get_device_total_memory(cls, device_id: int = 0) -> int:
raise NotImplementedError
@classmethod
def has_unified_memory(cls, device_id: int = 0) -> bool:
# This is cudaDeviceProp::integrated's driver-level source of truth. It
# is true on parts such as GB10 and Jetson whose GPU reads host memory.
return _cuda_driver_device_is_integrated(device_id)
@classmethod
def is_async_output_supported(cls, enforce_eager: bool | None) -> bool:
if enforce_eager:
logger.warning("To see benefits of async output processing, enable CUDA "
"graph. Since, enforce-eager is enabled, async output "
"processor cannot be used")
return False
return True
@classmethod
def is_full_nvlink(cls, device_ids: list[int]) -> bool:
raise NotImplementedError
@classmethod
def log_warnings(cls) -> None:
pass
@classmethod
def get_current_memory_usage(cls, device: torch.types.Device | None = None) -> float:
torch.cuda.reset_peak_memory_stats(device)
return float(torch.cuda.max_memory_allocated(device))
@classmethod
def get_torch_device(cls):
"""
Return torch.cuda
"""
return torch.cuda
@classmethod
def get_attn_backend_cls(cls, selected_backend: AttentionBackendEnum | None, head_size: int,
dtype: torch.dtype) -> str:
# TODO(will): maybe come up with a more general interface for local attention
# if distributed is False, we always try to use Flash attn
logger.info("Trying FASTVIDEO_ATTENTION_BACKEND=%s", envs.FASTVIDEO_ATTENTION_BACKEND)
logger.info("Selected backend: %s", selected_backend)
if selected_backend == AttentionBackendEnum.SAGE_ATTN:
try:
from sageattention import sageattn # noqa: F401
from fastvideo.attention.backends.sage_attn import ( # noqa: F401
SageAttentionBackend)
logger.info("Using Sage Attention backend.")
return "fastvideo.attention.backends.sage_attn.SageAttentionBackend"
except ImportError as e:
logger.info(e)
logger.info("Sage Attention backend is not installed. Fall back to Flash Attention.")
elif selected_backend == AttentionBackendEnum.SAGE_ATTN_THREE:
try:
from sageattn3 import sageattn3_blackwell # noqa: F401
from fastvideo.attention.backends.sage_attn3 import ( # noqa: F401
SageAttention3Backend)
logger.info("Using Sage Attention 3 backend.")
return "fastvideo.attention.backends.sage_attn3.SageAttention3Backend"
except ImportError as e:
logger.info(e)
logger.info("Sage Attention 3 backend is not installed. Fall back to Flash Attention.")
elif selected_backend == AttentionBackendEnum.ATTN_QAT_INFER:
from fastvideo.attention.backends.attn_qat_infer import ( # noqa: F401
AttnQatInferBackend, attn_qat_infer_receipt, is_attn_qat_infer_available)
if is_attn_qat_infer_available():
logger.info("Using Attn-QAT inference backend (%s).", attn_qat_infer_receipt())
return "fastvideo.attention.backends.attn_qat_infer.AttnQatInferBackend"
raise ImportError(
f"ATTN_QAT_INFER selected but the inference kernel is not usable ({attn_qat_infer_receipt()}). "
"Silent fallback would run plain FlashAttention while the caller believes it is measuring "
"FP4-QAT attention — an A/B comparison would silently benchmark bf16 against bf16; "
"refusing to proceed. Build the fastvideo-kernel attn_qat_infer target for this arch "
"or pick a different FASTVIDEO_ATTENTION_BACKEND.")
elif selected_backend == AttentionBackendEnum.ATTN_QAT_TRAIN:
from fastvideo.attention.backends.attn_qat_train import ( # noqa: F401
AttnQatTrainBackend, is_attn_qat_train_available)
if is_attn_qat_train_available():
logger.info("Using Attn-QAT training (fake-quantized attention) backend.")
return "fastvideo.attention.backends.attn_qat_train.AttnQatTrainBackend"
raise ImportError(
"ATTN_QAT_TRAIN selected but fastvideo_kernel.triton_kernels.attn_qat_train is not built. "
"Silent fallback would produce a non-QAT training run; refusing to proceed. "
"Install the training kernel or pick a different FASTVIDEO_ATTENTION_BACKEND.")
elif selected_backend == AttentionBackendEnum.NABLA_ATTN:
from fastvideo.attention.backends.nabla import CAN_USE_FLEX_ATTN
if CAN_USE_FLEX_ATTN:
logger.info("Using NABLA block-sparse flex-attention backend.")
return "fastvideo.attention.backends.nabla.NablaAttentionBackend"
raise ImportError("NABLA_ATTN selected but torch.nn.attention.flex_attention is unavailable in this "
"PyTorch build. Silent fallback to dense attention would be orders of magnitude "
"slower and diverge from the reference; upgrade PyTorch or pick a different backend.")
elif selected_backend == AttentionBackendEnum.VIDEO_SPARSE_ATTN:
try:
from fastvideo_kernel import video_sparse_attn # noqa: F401
from fastvideo.attention.backends.video_sparse_attn import ( # noqa: F401
VideoSparseAttentionBackend)
logger.info("Using Video Sparse Attention backend.")
return "fastvideo.attention.backends.video_sparse_attn.VideoSparseAttentionBackend"
except ImportError as e:
logger.error("Failed to import Video Sparse Attention backend: %s", str(e))
raise ImportError("The Video Sparse Attention backend is not installed. "
"To install it, please follow the instructions at: "
"https://hao-ai-lab.github.io/FastVideo/video_sparse_attention/installation ") from e
elif selected_backend == AttentionBackendEnum.VIDEO_SPARSE_ATTN_H3:
try:
from fastvideo_kernel.block_sparse_attn_256 import ( # noqa: F401
block_sparse_attn_256_bshd)
logger.info("Using MiniMax-H3 Video Sparse Attention backend.")
return "fastvideo.attention.backends.video_sparse_attn_h3.MiniMaxH3VSABackend"
except ImportError as e:
logger.error("Failed to import H3 Video Sparse Attention backend: %s", str(e))
raise ImportError("VIDEO_SPARSE_ATTN_H3 selected but fastvideo_kernel's block-sparse "
"kernels are unavailable. Install fastvideo-kernel or pick a different "
"FASTVIDEO_ATTENTION_BACKEND.") from e
elif selected_backend == AttentionBackendEnum.BSA_ATTN:
try:
from fastvideo.attention.backends.bsa_attn import ( # noqa: F401
BSAAttentionBackend)
logger.info("Using BSA Attention backend.")
return "fastvideo.attention.backends.bsa_attn.BSAAttentionBackend"
except ImportError as e:
logger.error("Failed to import BSA Attention backend: %s", str(e))
raise ImportError("The BSA Attention backend failed to import.") from e
elif selected_backend == AttentionBackendEnum.VMOBA_ATTN:
try:
from fastvideo_kernel import moba_attn_varlen # noqa: F401
from fastvideo.attention.backends.vmoba import ( # noqa: F401
VMOBAAttentionBackend)
logger.info("Using Video MOBA Attention backend.")
return "fastvideo.attention.backends.vmoba.VMOBAAttentionBackend"
except ImportError as e:
logger.error("Failed to import Video MoBA Attention backend: %s", str(e))
raise ImportError("Video MoBA Attention backend is not installed. ") from e
elif selected_backend == AttentionBackendEnum.SLA_ATTN:
try:
from fastvideo.attention.backends.sla import ( # noqa: F401
SLAAttentionBackend)
logger.info("Using SLA (Sparse-Linear Attention) backend.")
return "fastvideo.attention.backends.sla.SLAAttentionBackend"
except ImportError as e:
logger.error("Failed to import SLA Attention backend: %s", str(e))
raise ImportError("SLA Attention backend is not available. ") from e
elif selected_backend == AttentionBackendEnum.SAGE_SLA_ATTN:
try:
from fastvideo.attention.backends.sla import ( # noqa: F401
SageSLAAttentionBackend)
logger.info("Using SageSLA (Quantized Sparse-Linear Attention) backend.")
return "fastvideo.attention.backends.sla.SageSLAAttentionBackend"
except ImportError as e:
logger.error("Failed to import SageSLA Attention backend: %s", str(e))
raise ImportError("SageSLA Attention backend requires spas_sage_attn. "
"Install with: uv pip install git+https://github.com/thu-ml/SpargeAttn.git") from e
elif selected_backend == AttentionBackendEnum.TORCH_SDPA:
logger.info("Using Torch SDPA backend.")
return "fastvideo.attention.backends.sdpa.SDPABackend"
elif selected_backend == AttentionBackendEnum.FLASH_ATTN or selected_backend is None:
pass
elif selected_backend:
raise ValueError(f"Invalid attention backend for {cls.device_name}")
target_backend = AttentionBackendEnum.FLASH_ATTN
if not cls.has_device_capability(80):
logger.info("Cannot use FlashAttention-2 backend for Volta and Turing "
"GPUs.")
target_backend = AttentionBackendEnum.TORCH_SDPA
elif dtype not in (torch.float16, torch.bfloat16):
logger.info("Cannot use FlashAttention-2 backend for dtype other than "
"torch.float16 or torch.bfloat16.")
target_backend = AttentionBackendEnum.TORCH_SDPA
# FlashAttn is valid for the model, checking if the package is
# installed.
if target_backend == AttentionBackendEnum.FLASH_ATTN:
try:
import flash_attn # noqa: F401
from fastvideo.attention.backends.flash_attn import ( # noqa: F401
FlashAttentionBackend)
supported_sizes = \
FlashAttentionBackend.get_supported_head_sizes()
if head_size not in supported_sizes:
logger.info("Cannot use FlashAttention-2 backend for head size %d.", head_size)
target_backend = AttentionBackendEnum.TORCH_SDPA
except ImportError:
logger.info("Cannot use FlashAttention-2 backend because the "
"flash_attn package is not found. "
"Make sure that flash_attn was built and installed "
"(on by default).")
target_backend = AttentionBackendEnum.TORCH_SDPA
if target_backend == AttentionBackendEnum.TORCH_SDPA:
logger.info("Using Torch SDPA backend.")
return "fastvideo.attention.backends.sdpa.SDPABackend"
logger.info("Using Flash Attention backend.")
return "fastvideo.attention.backends.flash_attn.FlashAttentionBackend"
@classmethod
def get_device_communicator_cls(cls) -> str:
return "fastvideo.distributed.device_communicators.cuda_communicator.CudaCommunicator" # noqa
# NVML utils
# Note that NVML is not affected by `CUDA_VISIBLE_DEVICES`,
# all the related functions work on real physical device ids.
# the major benefit of using NVML is that it will not initialize CUDA
class NvmlCudaPlatform(CudaPlatformBase):
@classmethod
@lru_cache(maxsize=8)
@with_nvml_context
def get_device_capability(cls, device_id: int = 0) -> DeviceCapability | None:
try:
physical_device_id = device_id_to_physical_device_id(device_id)
handle = pynvml.nvmlDeviceGetHandleByIndex(physical_device_id)
major, minor = pynvml.nvmlDeviceGetCudaComputeCapability(handle)
return DeviceCapability(major=major, minor=minor)
except RuntimeError:
return None
@classmethod
@lru_cache(maxsize=8)
@with_nvml_context
def has_device_capability(
cls,
capability: tuple[int, int] | int,
device_id: int = 0,
) -> bool:
try:
return bool(super().has_device_capability(capability, device_id))
except RuntimeError:
return False
@classmethod
@lru_cache(maxsize=8)
@with_nvml_context
def get_device_name(cls, device_id: int = 0) -> str:
physical_device_id = device_id_to_physical_device_id(device_id)
return cls._get_physical_device_name(physical_device_id)
@classmethod
@lru_cache(maxsize=8)
@with_nvml_context
def get_device_uuid(cls, device_id: int = 0) -> str:
physical_device_id = device_id_to_physical_device_id(device_id)
handle = pynvml.nvmlDeviceGetHandleByIndex(physical_device_id)
return str(pynvml.nvmlDeviceGetUUID(handle))
@classmethod
@lru_cache(maxsize=8)
@with_nvml_context
def get_device_total_memory(cls, device_id: int = 0) -> int:
physical_device_id = device_id_to_physical_device_id(device_id)
handle = pynvml.nvmlDeviceGetHandleByIndex(physical_device_id)
return int(pynvml.nvmlDeviceGetMemoryInfo(handle).total)
@classmethod
@with_nvml_context
def is_full_nvlink(cls, physical_device_ids: list[int]) -> bool:
"""
query if the set of gpus are fully connected by nvlink (1 hop)
"""
handles = [pynvml.nvmlDeviceGetHandleByIndex(i) for i in physical_device_ids]
for i, handle in enumerate(handles):
for j, peer_handle in enumerate(handles):
if i < j:
try:
p2p_status = pynvml.nvmlDeviceGetP2PStatus(
handle,
peer_handle,
pynvml.NVML_P2P_CAPS_INDEX_NVLINK,
)
if p2p_status != pynvml.NVML_P2P_STATUS_OK:
return False
except pynvml.NVMLError:
logger.exception("NVLink detection failed. This is normal if"
" your machine has no NVLink equipped.")
return False
return True
@classmethod
def _get_physical_device_name(cls, device_id: int = 0) -> str:
handle = pynvml.nvmlDeviceGetHandleByIndex(device_id)
return str(pynvml.nvmlDeviceGetName(handle))
@classmethod
@with_nvml_context
def log_warnings(cls) -> None:
device_ids: int = pynvml.nvmlDeviceGetCount()
if device_ids > 1:
device_names = [cls._get_physical_device_name(i) for i in range(device_ids)]
if (len(set(device_names)) > 1 and os.environ.get("CUDA_DEVICE_ORDER") != "PCI_BUS_ID"):
logger.warning(
"Detected different devices in the system: %s. Please"
" make sure to set `CUDA_DEVICE_ORDER=PCI_BUS_ID` to "
"avoid unexpected behavior.",
", ".join(device_names),
)
class NonNvmlCudaPlatform(CudaPlatformBase):
@classmethod
def get_device_capability(cls, device_id: int = 0) -> DeviceCapability:
major, minor = torch.cuda.get_device_capability(device_id)
return DeviceCapability(major=major, minor=minor)
@classmethod
def get_device_name(cls, device_id: int = 0) -> str:
return str(torch.cuda.get_device_name(device_id))
@classmethod
def get_device_total_memory(cls, device_id: int = 0) -> int:
device_props = torch.cuda.get_device_properties(device_id)
return int(device_props.total_memory)
@classmethod
def is_full_nvlink(cls, physical_device_ids: list[int]) -> bool:
logger.exception("NVLink detection not possible, as context support was"
" not found. Assuming no NVLink available.")
return False
# Autodetect either NVML-enabled or non-NVML platform
# based on whether NVML is available.
nvml_available = False
try:
try:
pynvml.nvmlInit()
nvml_available = True
except Exception:
# On Jetson, NVML is not supported.
nvml_available = False
finally:
if nvml_available:
pynvml.nvmlShutdown()
CudaPlatform = NvmlCudaPlatform if nvml_available else NonNvmlCudaPlatform
try:
from sphinx.ext.autodoc.mock import _MockModule
if not isinstance(pynvml, _MockModule):
CudaPlatform.log_warnings()
except ModuleNotFoundError:
CudaPlatform.log_warnings()