# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import ctypes import functools import os from collections.abc import Callable from typing import TYPE_CHECKING, Any import torch import torch.distributed as dist from torch._ops import OpOverload import vllm.envs as envs from vllm.logger import init_logger from vllm.platforms import current_platform from vllm.utils.torch_utils import direct_register_custom_op from vllm.v1.attention.ops.rocm_aiter_mla_sparse import ( rocm_aiter_sparse_attn_indexer, rocm_aiter_sparse_attn_indexer_fake, ) if TYPE_CHECKING: from aiter import ActivationType, QuantType logger = init_logger(__name__) # fp8_dtype is not cached. # on ROCm the fp8_dtype always calls is_fp8_fnuz # which is a host op, so we cache it once here. FP8_DTYPE = current_platform.fp8_dtype() _HIPB_MM_INITIALIZED_DEVICES: set[int] = set() _FP8_E4M3_DTYPES = { dtype for dtype in ( getattr(torch, "float8_e4m3fn", None), getattr(torch, "float8_e4m3fnuz", None), ) if dtype is not None } KB = 1024 MB = 1024 * KB def _is_fp8_e4m3_tensor(t: torch.Tensor | None) -> bool: return t is not None and t.dtype in _FP8_E4M3_DTYPES def _get_or_create_aiter_qr_rmsnorm_comm( device_comm: Any, ) -> tuple[int, int, bool] | None: """Create the AITER QR communicator used by fused QR+RMSNorm. vLLM's normal QuickReduce communicator owns vLLM C++ QR buffers. The fused QR+RMSNorm kernel added in AITER needs an AITER DeviceComms instance, so we lazily create one on the same TP CPU group and keep it on the vLLM device communicator for reuse. """ existing = getattr(device_comm, "_aiter_qr_rmsnorm_comm", None) if existing is False: return None if existing is not None: return existing try: import aiter except Exception: device_comm._aiter_qr_rmsnorm_comm = False return None if not hasattr(aiter, "qr_all_reduce_rmsnorm"): device_comm._aiter_qr_rmsnorm_comm = False return None group = getattr(device_comm, "cpu_group", None) if group is None: device_comm._aiter_qr_rmsnorm_comm = False return None world_size = dist.get_world_size(group=group) rank = dist.get_rank(group=group) if world_size not in (2, 4, 8): device_comm._aiter_qr_rmsnorm_comm = False return None qr_max_size_mb = envs.VLLM_ROCM_QUICK_REDUCE_MAX_SIZE_BYTES_MB qr_max_size = None if qr_max_size_mb is None else qr_max_size_mb * MB try: ptr = aiter.init_custom_qr(rank, world_size, qr_max_size) handle = aiter.qr_get_handle(ptr) handles = [None] * world_size dist.all_gather_object(handles, handle, group=group) aiter.qr_open_handles(ptr, handles) except Exception: device_comm._aiter_qr_rmsnorm_comm = False return None comm = (ptr, world_size, envs.VLLM_ROCM_QUICK_REDUCE_CAST_BF16_TO_FP16) device_comm._aiter_qr_rmsnorm_comm = comm return comm def _ensure_hipb_mm_extension_initialized() -> None: import aiter device = torch.accelerator.current_device_index() if device not in _HIPB_MM_INITIALIZED_DEVICES: aiter.hipb_create_extension() _HIPB_MM_INITIALIZED_DEVICES.add(device) def is_aiter_found() -> bool: from importlib.util import find_spec return find_spec("aiter") is not None # `find_spec` is not torch.compile compatible. # In cases where aiter availability might have # been checked in forward passes that are torch compiled. # we keep this global outside to not cause torch compile breaks. IS_AITER_FOUND = is_aiter_found() class _DlInfo(ctypes.Structure): _fields_ = [ ("dli_fname", ctypes.c_char_p), ("dli_fbase", ctypes.c_void_p), ("dli_sname", ctypes.c_char_p), ("dli_saddr", ctypes.c_void_p), ] _HIP_RUNTIME_HANDLE: ctypes.CDLL | None = None def _find_torch_hip_runtime_path() -> str | None: """Find the HIP runtime already selected by PyTorch's extension graph.""" try: torch_extension = ctypes.CDLL(torch._C.__file__) hip_symbol = torch_extension.hipGetDeviceCount dladdr = ctypes.CDLL(None).dladdr dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(_DlInfo)] dladdr.restype = ctypes.c_int info = _DlInfo() if not dladdr(ctypes.cast(hip_symbol, ctypes.c_void_p), ctypes.byref(info)): return None except (AttributeError, OSError, TypeError, ctypes.ArgumentError): return None if info.dli_fname is None: return None runtime_path = os.fsdecode(info.dli_fname) if "libamdhip64.so" not in os.path.basename(runtime_path): return None return runtime_path def _promote_torch_hip_runtime() -> None: """Make PyTorch's HIP runtime symbols visible to AITER JIT extensions. PyTorch loads its native extension with ``RTLD_LOCAL``. Some AITER JIT extensions rely on HIP registration and launch symbols being available in the process-global lookup scope. Reopening the exact runtime that provides PyTorch's ``hipGetDeviceCount`` symbol promotes the existing DSO instead of selecting or initializing a second HIP runtime. """ global _HIP_RUNTIME_HANDLE if _HIP_RUNTIME_HANDLE is not None: return runtime_path = _find_torch_hip_runtime_path() if runtime_path is None: logger.warning_once( "Could not locate PyTorch's HIP runtime; AITER JIT extensions may " "fail to resolve HIP symbols." ) return try: # Retain the handle for the process lifetime. Besides preventing a # future dlclose, this documents that the RTLD_GLOBAL promotion is # intentional and must outlive every lazily imported AITER extension. _HIP_RUNTIME_HANDLE = ctypes.CDLL(runtime_path, mode=ctypes.RTLD_GLOBAL) except OSError as err: logger.warning_once( "Could not promote PyTorch's HIP runtime at %s; AITER JIT " "extensions may fail to resolve HIP symbols: %s", runtime_path, err, ) def _maybe_promote_torch_hip_runtime() -> None: if current_platform.is_rocm() and IS_AITER_FOUND: _promote_torch_hip_runtime() _maybe_promote_torch_hip_runtime() def is_aiter_found_and_supported() -> bool: """Check if AITER library is available and platform supports it. Checks: platform (ROCm), device arch is CDNA 3 or better, and library existence. Does NOT check environment variables - that's handled by rocm_aiter_ops.is_enabled(). This function determines if aiter CAN be used, not if it SHOULD be used. Separation of concerns: - This function: Can aiter work on this system? (platform + library availability) - rocm_aiter_ops.is_enabled(): Should aiter be used by default? (adds env var check) - Backend selection: Can explicitly request aiter regardless of env var This allows explicit backend selection via attention_config to work even when VLLM_ROCM_USE_AITER=0, while preventing unwanted JIT warnings for auto-discovery. """ if current_platform.is_rocm() and IS_AITER_FOUND: from vllm.platforms.rocm import get_cdna_version return get_cdna_version() > 2 return False def is_aiter_found_and_supported_on_rdna4() -> bool: """RDNA4 (gfx12) analog of `is_aiter_found_and_supported()`. gfx12 has no aiter CK build, so this deliberately stays off the gfx9 `@if_aiter_supported` umbrella; it reports only that aiter's Triton kernels are usable here. Like its gfx9 counterpart it checks platform + arch + library availability and does not check environment variables. """ if current_platform.is_rocm() and IS_AITER_FOUND: from vllm.platforms.rocm import on_rdna4 return on_rdna4() return False def _triton_gemm_config_is_tuned(config_name: str, N: int, K: int) -> bool: from aiter.ops.triton.utils.gemm_config_utils import get_gemm_config # any M shape under 1024 should be tuned M_TUNE_PROBE = 128 return get_gemm_config(config_name, M_TUNE_PROBE, N, K)[1] def _ck_gemm_shape_is_tuned( N: int, K: int, q_dtype_w: torch.dtype, csv_attr: str ) -> bool: l_m = ( [1, 2, 4] + list(range(8, 513, 8)) + [1024, 1536] + [2**i for i in range(11, 19)] ) try: from aiter.ops.gemm_op_a8w8 import ( AITER_CONFIGS, get_GEMM_config_with_quant_type, ) csv_path = getattr(AITER_CONFIGS, csv_attr) return any( get_GEMM_config_with_quant_type(M, N, K, q_dtype_w, csv_path) is not None for M in l_m ) except (AttributeError, ImportError, OSError): logger.warning_once( "Could not read aiter CK GEMM configs from AITER_CONFIGS.%s; " "treating all shapes as untuned.", csv_attr, ) return False def if_aiter_supported(func: Callable) -> Callable: """Decorator that only executes the function if ROCm AITER package is supported and enabled on gfx9 archs. """ @functools.wraps(func) def wrapper(*args, **kwargs): if is_aiter_found_and_supported(): return func(*args, **kwargs) return None return wrapper def _validate_rocm_aiter_fused_moe_shared_expert_args( shared_w1: torch.Tensor | None, shared_w2: torch.Tensor | None, shared_w1_scale: torch.Tensor | None, shared_w2_scale: torch.Tensor | None, shared_expert_id: int, ) -> bool: shared_tensors = (shared_w1, shared_w2, shared_w1_scale, shared_w2_scale) has_shared_expert = any(tensor is not None for tensor in shared_tensors) if has_shared_expert: if not all(tensor is not None for tensor in shared_tensors): raise ValueError( "Heterogeneous fused MoE requires both shared weights and scales." ) if shared_expert_id < 0: raise ValueError( "Heterogeneous fused MoE requires a non-negative shared expert ID." ) elif shared_expert_id >= 0: raise ValueError( "A non-negative shared expert ID requires shared weights and scales." ) return has_shared_expert def _rocm_aiter_fused_moe_impl( hidden_states: torch.Tensor, w1: torch.Tensor, w2: torch.Tensor, topk_weight: torch.Tensor, topk_ids: torch.Tensor, expert_mask: torch.Tensor | None = None, activation_method: int = 0, quant_method: int = 0, doweight_stage1: bool = False, w1_scale: torch.Tensor | None = None, w2_scale: torch.Tensor | None = None, a1_scale: torch.Tensor | None = None, a2_scale: torch.Tensor | None = None, num_local_tokens: torch.Tensor | None = None, output_dtype: torch.dtype | None = None, hidden_pad: int = 0, intermediate_pad: int = 0, gate_mode: str = "", bias1: torch.Tensor | None = None, bias2: torch.Tensor | None = None, moe_sorting_dispatch_policy: int = 0, swiglu_limit: float = 0.0, beta: float | None = None, linear_beta: float | None = None, shared_w1: torch.Tensor | None = None, shared_w2: torch.Tensor | None = None, shared_w1_scale: torch.Tensor | None = None, shared_w2_scale: torch.Tensor | None = None, shared_expert_id: int = -1, q_dtype_a: torch.dtype | None = None, ) -> torch.Tensor: has_shared_expert = _validate_rocm_aiter_fused_moe_shared_expert_args( shared_w1, shared_w2, shared_w1_scale, shared_w2_scale, shared_expert_id, ) from aiter import ActivationType, QuantType from aiter.fused_moe import fused_moe activation = ActivationType(activation_method) quant_type = QuantType(quant_method) extra_kwargs: dict = {} if gate_mode and rocm_aiter_ops.fused_moe_supports_gate_mode(): extra_kwargs["gate_mode"] = gate_mode if ( getattr(ActivationType, "Situv2", None) is not None and activation == ActivationType.Situv2 ): extra_kwargs["beta"] = beta extra_kwargs["linear_beta"] = linear_beta if has_shared_expert: extra_kwargs.update( shared_w1=shared_w1, shared_w2=shared_w2, shared_w1_scale=shared_w1_scale, shared_w2_scale=shared_w2_scale, shared_expert_id=shared_expert_id, ) if q_dtype_a is not None: # DeepSeek V4.1 a4w4 override (use_mxfp4_w4a4_dsv4 in # rocm_aiter_moe.py). rocm_aiter_ops.fused_moe_supports_quant_dtype_a() # is checked at config time, so this is only reached on an AITER # build new enough to accept it. extra_kwargs["quant_dtype_a"] = q_dtype_a return fused_moe( hidden_states, w1, w2, topk_weight, topk_ids, expert_mask, activation, quant_type, doweight_stage1, w1_scale, w2_scale, a1_scale, a2_scale, num_local_tokens=num_local_tokens, dtype=output_dtype, hidden_pad=hidden_pad, intermediate_pad=intermediate_pad, bias1=bias1, bias2=bias2, moe_sorting_dispatch_policy=moe_sorting_dispatch_policy, swiglu_limit=swiglu_limit, **extra_kwargs, ) def _rocm_aiter_fused_moe_fake( hidden_states: torch.Tensor, w1: torch.Tensor, w2: torch.Tensor, topk_weight: torch.Tensor, topk_ids: torch.Tensor, expert_mask: torch.Tensor | None = None, activation_method: int = 0, quant_method: int = 0, doweight_stage1: bool = False, w1_scale: torch.Tensor | None = None, w2_scale: torch.Tensor | None = None, a1_scale: torch.Tensor | None = None, a2_scale: torch.Tensor | None = None, num_local_tokens: torch.Tensor | None = None, output_dtype: torch.dtype | None = None, hidden_pad: int = 0, intermediate_pad: int = 0, gate_mode: str = "", bias1: torch.Tensor | None = None, bias2: torch.Tensor | None = None, moe_sorting_dispatch_policy: int = 0, swiglu_limit: float = 0.0, beta: float | None = None, linear_beta: float | None = None, shared_w1: torch.Tensor | None = None, shared_w2: torch.Tensor | None = None, shared_w1_scale: torch.Tensor | None = None, shared_w2_scale: torch.Tensor | None = None, shared_expert_id: int = -1, q_dtype_a: torch.dtype | None = None, ) -> torch.Tensor: if output_dtype is not None: return torch.empty_like(hidden_states, dtype=output_dtype) return torch.empty_like(hidden_states) def _rocm_aiter_asm_moe_tkw1_impl( hidden_states: torch.Tensor, w1: torch.Tensor, w2: torch.Tensor, topk_weights: torch.Tensor, topk_ids: torch.Tensor, fc1_scale: torch.Tensor | None = None, fc2_scale: torch.Tensor | None = None, fc1_smooth_scale: torch.Tensor | None = None, fc2_smooth_scale: torch.Tensor | None = None, a16: bool = False, per_tensor_quant_scale: torch.Tensor | None = None, expert_mask: torch.Tensor | None = None, activation_method: int = 0, ) -> torch.Tensor: from aiter import ActivationType from aiter.fused_moe_bf16_asm import asm_moe_tkw1 activation = ActivationType(activation_method) return asm_moe_tkw1( hidden_states, w1, w2, topk_weights, topk_ids, fc1_scale=fc1_scale, fc2_scale=fc2_scale, fc1_smooth_scale=fc1_smooth_scale, fc2_smooth_scale=fc2_smooth_scale, a16=a16, per_tensor_quant_scale=per_tensor_quant_scale, expert_mask=expert_mask, activation=activation, ) def _rocm_aiter_asm_moe_tkw1_fake( hidden_states: torch.Tensor, w1: torch.Tensor, w2: torch.Tensor, topk_weights: torch.Tensor, topk_ids: torch.Tensor, fc1_scale: torch.Tensor | None = None, fc2_scale: torch.Tensor | None = None, fc1_smooth_scale: torch.Tensor | None = None, fc2_smooth_scale: torch.Tensor | None = None, a16: bool = False, per_tensor_quant_scale: torch.Tensor | None = None, expert_mask: torch.Tensor | None = None, activation_method: int = 0, ) -> torch.Tensor: return torch.empty_like(hidden_states) def _rocm_aiter_topk_softmax_impl( topk_weights: torch.Tensor, topk_indices: torch.Tensor, token_expert_indices: torch.Tensor, gating_output: torch.Tensor, renormalize: bool, num_shared_experts: int = 0, shared_expert_scoring_func: str = "", ) -> None: from aiter import topk_softmax topk_softmax( topk_weights, topk_indices, token_expert_indices, gating_output, renormalize, num_shared_experts, shared_expert_scoring_func, ) def _rocm_aiter_topk_sigmoid_impl( topk_weights: torch.Tensor, topk_indices: torch.Tensor, gating_output: torch.Tensor, ) -> None: from aiter import topk_sigmoid topk_sigmoid(topk_weights, topk_indices, gating_output) def _rocm_aiter_biased_grouped_topk_impl( gating_output: torch.Tensor, correction_bias: torch.Tensor, topk_weights: torch.Tensor, topk_ids: torch.Tensor, num_expert_group: int, topk_group: int, need_renorm: bool, routed_scaling_factor: float = 1.0, # mul to topk_weights ) -> None: from aiter import biased_grouped_topk biased_grouped_topk( gating_output, correction_bias, topk_weights, topk_ids, num_expert_group, topk_group, need_renorm, routed_scaling_factor, ) def _rocm_aiter_grouped_topk_impl( gating_output: torch.Tensor, topk_weights: torch.Tensor, topk_ids: torch.Tensor, num_expert_group: int, topk_group: int, need_renorm: bool, scoring_func: str = "softmax", routed_scaling_factor: float = 1.0, # mul to topk_weights ) -> None: is_softmax = scoring_func == "softmax" from aiter import grouped_topk grouped_topk( gating_output, topk_weights, topk_ids, num_expert_group, topk_group, need_renorm, is_softmax, routed_scaling_factor, ) def _rocm_aiter_fused_topk_impl( x: torch.Tensor, router_logits: torch.Tensor, top_k: int, gate_up: bool, ) -> tuple[torch.Tensor, torch.Tensor]: from aiter.fused_moe import fused_topk # fused_topk returns (topk_weights, topk_indices) return fused_topk(x, router_logits, top_k, gate_up) def _rocm_aiter_fused_topk_fake( x: torch.Tensor, router_logits: torch.Tensor, top_k: int, gate_up: bool, ) -> tuple[torch.Tensor, torch.Tensor]: num_tokens = x.shape[0] topk_weights = torch.empty( (num_tokens, top_k), dtype=torch.float32, device=x.device ) topk_indices = torch.empty((num_tokens, top_k), dtype=torch.int32, device=x.device) return topk_weights, topk_indices # Cache whether aiter supports FP8 MLA parameters _AITER_MLA_SUPPORTS_FP8: bool | None = None def _check_aiter_mla_fp8_support() -> bool: """Check if aiter.mla.mla_decode_fwd supports q_scale and kv_scale parameters.""" global _AITER_MLA_SUPPORTS_FP8 if _AITER_MLA_SUPPORTS_FP8 is None: try: import inspect from aiter.mla import mla_decode_fwd sig = inspect.signature(mla_decode_fwd) _AITER_MLA_SUPPORTS_FP8 = ( "q_scale" in sig.parameters and "kv_scale" in sig.parameters ) except ( ImportError, ModuleNotFoundError, AttributeError, ValueError, TypeError, ): # ImportError/ModuleNotFoundError: aiter.mla module not available # AttributeError: mla_decode_fwd doesn't exist # ValueError: mla_decode_fwd has no signature (e.g., built-in) # TypeError: mla_decode_fwd is not a callable _AITER_MLA_SUPPORTS_FP8 = False return _AITER_MLA_SUPPORTS_FP8 def _rocm_aiter_gather_kv_b_proj_impl( kv_cache: torch.Tensor, kv_scale: torch.Tensor, kv_indptr: torch.Tensor, kv_indices: torch.Tensor, cu_seq_lens: torch.Tensor, weight: torch.Tensor, k: torch.Tensor, v: torch.Tensor, ) -> None: from aiter.ops.triton.gather_kv_b_proj import gather_kv_b_proj gather_kv_b_proj( kv_cache, kv_scale, kv_indptr, kv_indices, cu_seq_lens, weight, None, k, v ) def _rocm_aiter_gather_kv_b_proj_fake( kv_cache: torch.Tensor, kv_scale: torch.Tensor, kv_indptr: torch.Tensor, kv_indices: torch.Tensor, cu_seq_lens: torch.Tensor, weight: torch.Tensor, k: torch.Tensor, v: torch.Tensor, ) -> None: pass def _rocm_aiter_mla_decode_fwd_impl( q: torch.Tensor, kv_buffer: torch.Tensor, o: torch.Tensor, qo_indptr: torch.Tensor, max_seqlen_qo: int, kv_indptr: torch.Tensor | None = None, kv_indices: torch.Tensor | None = None, kv_last_page_lens: torch.Tensor | None = None, sm_scale: float = 1.0, logit_cap: float = 0.0, q_scale: torch.Tensor | None = None, kv_scale: torch.Tensor | None = None, work_meta_data: torch.Tensor | None = None, work_indptr: torch.Tensor | None = None, work_info_set: torch.Tensor | None = None, reduce_indptr: torch.Tensor | None = None, reduce_final_map: torch.Tensor | None = None, reduce_partial_map: torch.Tensor | None = None, causal: bool = True, ) -> None: from aiter.mla import mla_decode_fwd kwargs: dict[str, float | torch.Tensor | None | bool] = { "sm_scale": sm_scale, "logit_cap": logit_cap, } # Only pass q_scale and kv_scale if the aiter library supports them if _check_aiter_mla_fp8_support(): kwargs["q_scale"] = q_scale kwargs["kv_scale"] = kv_scale kwargs["causal"] = causal if work_meta_data is not None: assert work_indptr is not None, ( "work_indptr must be provided with work_meta_data" ) assert work_info_set is not None, ( "work_info_set must be provided with work_meta_data" ) assert reduce_indptr is not None, ( "reduce_indptr must be provided with work_meta_data" ) assert reduce_final_map is not None, ( "reduce_final_map must be provided with work_meta_data" ) assert reduce_partial_map is not None, ( "reduce_partial_map must be provided with work_meta_data" ) kwargs["work_meta_data"] = work_meta_data kwargs["work_indptr"] = work_indptr kwargs["work_info_set"] = work_info_set kwargs["reduce_indptr"] = reduce_indptr kwargs["reduce_final_map"] = reduce_final_map kwargs["reduce_partial_map"] = reduce_partial_map mla_decode_fwd( q, kv_buffer.view(-1, 1, 1, q.shape[-1]), o, qo_indptr, kv_indptr, kv_indices, kv_last_page_lens, max_seqlen_qo, **kwargs, ) def _rocm_aiter_mla_decode_fwd_lse_impl( q: torch.Tensor, kv_buffer: torch.Tensor, o: torch.Tensor, qo_indptr: torch.Tensor, max_seqlen_qo: int, kv_indptr: torch.Tensor | None = None, kv_indices: torch.Tensor | None = None, kv_last_page_lens: torch.Tensor | None = None, sm_scale: float = 1.0, logit_cap: float = 0.0, q_scale: torch.Tensor | None = None, kv_scale: torch.Tensor | None = None, ) -> torch.Tensor: """Run non-persistent AITER MLA decode and return natural-log LSE. gfx942 persistent MLA kernels do not provide an LSE code object. Keeping this as a separate op makes that constraint structural: callers that need LSE cannot accidentally forward persistent work metadata. """ from aiter.mla import get_meta_param, mla_decode_fwd kwargs: dict[str, float | int | torch.Tensor | None | bool] = { "sm_scale": sm_scale, "logit_cap": logit_cap, "return_lse": True, } if _check_aiter_mla_fp8_support(): kwargs["q_scale"] = q_scale kwargs["kv_scale"] = kv_scale if ( q.dtype == torch.bfloat16 and kv_buffer.dtype == torch.bfloat16 and q.shape[1] == 32 ): from vllm.platforms.rocm import on_gfx950 if on_gfx950(): # The gfx950 H32 BF16 kernel mishandles ragged tails with split KV. # Its single-split path returns correct output and natural-log LSE. kwargs["num_kv_splits"] = 1 kwargs["num_kv_splits_indptr"] = torch.arange( qo_indptr.shape[0], dtype=torch.int32, device=q.device ) if q.dtype == FP8_DTYPE: assert kv_indices is not None num_kv_splits, num_kv_splits_indptr = get_meta_param( None, qo_indptr.shape[0] - 1, kv_indices.numel(), q.shape[1], max_seqlen_qo, q.dtype, ) if num_kv_splits == 1: # gfx942's one-split FP8 asm writes the final output directly but # does not write either LSE buffer. Force the normal split reducer, # which produces both the same output and an accurate natural LSE. num_kv_splits = 2 num_kv_splits_indptr = torch.arange( 0, (qo_indptr.shape[0]) * num_kv_splits, num_kv_splits, dtype=torch.int32, device=q.device, ) kwargs["num_kv_splits"] = num_kv_splits kwargs["num_kv_splits_indptr"] = num_kv_splits_indptr _, final_lse = mla_decode_fwd( q, kv_buffer.view(-1, 1, 1, q.shape[-1]), o, qo_indptr, kv_indptr, kv_indices, kv_last_page_lens, max_seqlen_qo, **kwargs, ) if final_lse is None: raise RuntimeError("AITER MLA decode did not return the requested LSE") return final_lse def _rocm_aiter_mla_decode_fwd_lse_fake( q: torch.Tensor, kv_buffer: torch.Tensor, o: torch.Tensor, qo_indptr: torch.Tensor, max_seqlen_qo: int, kv_indptr: torch.Tensor | None = None, kv_indices: torch.Tensor | None = None, kv_last_page_lens: torch.Tensor | None = None, sm_scale: float = 1.0, logit_cap: float = 0.0, q_scale: torch.Tensor | None = None, kv_scale: torch.Tensor | None = None, ) -> torch.Tensor: return torch.empty(q.shape[:-1], dtype=torch.float32, device=q.device) def _rocm_aiter_w8a8_gemm_impl( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: from aiter import gemm_a8w8_CK # gemm_a8w8_CK(a, b, scale_a, scale_b, bias) expects # a to be [M, K] # b to be [N, K] # CutlassInt8ScaledMMLinearKernel prepare weight `w_q` in [K, N] format return gemm_a8w8_CK(A, B, As, Bs, bias, output_dtype) def _rocm_aiter_w8a8_gemm_fake( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: m = A.shape[0] n = B.shape[0] Y = torch.empty(m, n, dtype=output_dtype, device=A.device) return Y def _rocm_aiter_preshuffled_per_token_w8a8_gemm_impl( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: from aiter import gemm_a8w8_bpreshuffle output = gemm_a8w8_bpreshuffle(A, B, As, Bs, None, output_dtype) if bias is not None: output.add_(bias) return output def _rocm_aiter_preshuffled_per_token_w8a8_gemm_fake( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: m = A.shape[0] n = B.shape[0] return torch.empty(m, n, dtype=output_dtype, device=A.device) def _rocm_aiter_hipb_mm_fp8_impl( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.bfloat16, ) -> torch.Tensor: from aiter import hipb_mm _ensure_hipb_mm_extension_initialized() return hipb_mm( A, B, solution_index=-1, bias=bias, out_dtype=output_dtype, scaleA=As, scaleB=Bs, scaleOut=None, bpreshuffle=True, ) def _rocm_aiter_hipb_mm_fp8_fake( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.bfloat16, ) -> torch.Tensor: m = A.shape[0] n = B.shape[1] return torch.empty(m, n, dtype=output_dtype, device=A.device) def _rocm_aiter_triton_gemm_a8w8_blockscale_impl( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: from aiter.ops.triton.gemm_a8w8_blockscale import gemm_a8w8_blockscale return gemm_a8w8_blockscale(A, B, As, Bs, dtype=output_dtype) def _rocm_aiter_triton_gemm_a8w8_blockscale_fake( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: m = A.shape[0] n = B.shape[0] Y = torch.empty(m, n, dtype=output_dtype, device=A.device) return Y def _rocm_aiter_gemm_a8w8_blockscale_impl( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: from aiter import gemm_a8w8_blockscale return gemm_a8w8_blockscale(A, B, As, Bs, dtype=output_dtype) def _rocm_aiter_gemm_a8w8_blockscale_fake( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: m = A.shape[0] n = B.shape[0] Y = torch.empty(m, n, dtype=output_dtype, device=A.device) return Y def _rocm_aiter_gemm_a8w8_blockscale_bpreshuffle_impl( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: from aiter import gemm_a8w8_blockscale_bpreshuffle # B preshuffled (shuffle_weight (16,16)); As column-major group scale. return gemm_a8w8_blockscale_bpreshuffle(A, B, As, Bs, dtype=output_dtype) def _rocm_aiter_gemm_a8w8_blockscale_bpreshuffle_fake( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: m = A.shape[0] n = B.shape[0] Y = torch.empty(m, n, dtype=output_dtype, device=A.device) return Y def _rocm_aiter_rmsnorm_fused_add_dynamic_quant_impl( x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, quant_dtype: torch.dtype, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: import aiter as rocm_aiter assert quant_dtype in [torch.int8, FP8_DTYPE] y_scale = torch.empty(x.shape[0], 1, dtype=torch.float32, device=x.device) out = torch.empty(x.shape, dtype=quant_dtype, device=x.device) residual_out = torch.empty_like(x) rocm_aiter.rmsnorm2d_fwd_with_add_dynamicquant( out, x, residual, residual_out, y_scale, weight, epsilon, use_model_sensitive_rmsnorm=0, ) return out, residual_out, y_scale def _rocm_aiter_rmsnorm_fused_add_dynamic_quant_fake( x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, quant_dtype: torch.dtype, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: y_scale = torch.empty(x.shape[0], 1, dtype=torch.float32, device=x.device) out = torch.empty(x.shape, dtype=quant_dtype, device=x.device) residual_out = torch.empty_like(x) return out, residual_out, y_scale def _rocm_aiter_rmsnorm_fused_dynamic_quant_impl( x: torch.Tensor, weight: torch.Tensor, epsilon: float, quant_dtype: torch.dtype, ) -> tuple[torch.Tensor, torch.Tensor]: import aiter as rocm_aiter assert quant_dtype in [torch.int8, FP8_DTYPE] y_scale = torch.empty(x.shape[0], 1, dtype=torch.float32, device=x.device) out = torch.empty(x.shape, dtype=quant_dtype, device=x.device) rocm_aiter.rmsnorm2d_fwd_with_dynamicquant( out, x, y_scale, weight, epsilon, use_model_sensitive_rmsnorm=0 ) return out, y_scale def _rocm_aiter_rmsnorm_fused_dynamic_quant_fake( x: torch.Tensor, weight: torch.Tensor, epsilon: float, quant_dtype: torch.dtype, ) -> tuple[torch.Tensor, torch.Tensor]: y_scale = torch.empty(x.shape[0], 1, dtype=torch.float32, device=x.device) out = torch.empty(x.shape, dtype=quant_dtype, device=x.device) return out, y_scale def _rocm_aiter_fused_allreduce_rmsnorm_impl( input_: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, gemma_norm: bool = False, ) -> tuple[torch.Tensor, torch.Tensor]: aiter_ar = rocm_aiter_ops.get_aiter_allreduce() assert aiter_ar is not None, "aiter allreduce must be initialized" ca = aiter_ar.aiter_ca use_1stage = aiter_ar.use_1stage_fused_ar_rms(input_) if not use_1stage: from vllm.distributed import get_tp_group device_comm = get_tp_group().device_communicator qr_comm = getattr(device_comm, "qr_comm", None) hidden_dim = input_.shape[-1] row_size = hidden_dim * input_.element_size() fused_qr_rmsnorm_ok = ( # aiter.qr_all_reduce_rmsnorm does not implement Gemma's # (1 + weight) scaling. not gemma_norm and qr_comm is not None and not getattr(qr_comm, "disabled", True) and hasattr(qr_comm, "should_quick_allreduce") and qr_comm.should_quick_allreduce(input_) and input_.shape == residual.shape and input_.dtype in (torch.bfloat16, torch.float16) and input_.dtype == residual.dtype and input_.dtype == weight.dtype and weight.dim() == 1 and weight.numel() == hidden_dim and input_.numel() % hidden_dim == 0 and row_size > 0 and row_size <= 32 * KB and (32 * KB) % row_size == 0 ) if fused_qr_rmsnorm_ok: assert qr_comm is not None aiter_qr_comm = _get_or_create_aiter_qr_rmsnorm_comm(device_comm) if aiter_qr_comm is not None: import aiter ptr, _, cast_bf2half = aiter_qr_comm quant_level = qr_comm._get_qr_quant_level(input_) out = torch.empty_like(input_) residual_out = torch.empty_like(residual) aiter.qr_all_reduce_rmsnorm( ptr, input_, residual, residual_out, out, weight, epsilon, hidden_dim, quant_level, cast_bf2half, ) return out, residual_out result = ca.custom_fused_ar_rms( input_, residual, weight, epsilon, use_1stage=use_1stage, gemma_norm=gemma_norm, ) if result is None: from vllm.distributed import tensor_model_parallel_all_reduce return torch.ops.vllm_aiter.fused_add_rms_norm( tensor_model_parallel_all_reduce(input_), residual, weight, epsilon, gemma_norm, ) return result[0], result[1] def _rocm_aiter_fused_allreduce_rmsnorm_fake( input_: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, gemma_norm: bool = False, ) -> tuple[torch.Tensor, torch.Tensor]: return torch.empty_like(input_), torch.empty_like(residual) def _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_impl( input_: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Fused AllReduce + RMSNorm + per-group FP8 quant.""" aiter_ar = rocm_aiter_ops.get_aiter_allreduce() assert aiter_ar is not None, "aiter allreduce must be initialized" ca = aiter_ar.aiter_ca use_1stage = aiter_ar.use_1stage_fused_ar_rms(input_) result = ca.fused_ar_rms_per_group_quant( input_, residual, w=weight, eps=epsilon, group_size=group_size, registered=torch.cuda.is_current_stream_capturing() and ca.enable_register_for_capturing, use_1stage=use_1stage, ) assert result is not None return result[0], result[1], result[2] def _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_fake( input_: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: hidden_dim = input_.shape[-1] num_groups = hidden_dim // group_size quant_out = torch.empty(input_.shape, dtype=FP8_DTYPE, device=input_.device) residual_out = torch.empty_like(residual) scale_out = torch.empty( input_.shape[:-1] + (num_groups,), dtype=torch.float32, device=input_.device, ) return quant_out, residual_out, scale_out def _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_impl( input_: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Fused AllReduce + add-RMSNorm + per-group FP8 quant + bf16 normed act. Wraps the same AITER launcher as ``_rocm_aiter_fused_allreduce_rmsnorm_ quant_per_group_impl`` with ``emit_bf16=True``, emitting the pre-quant bf16/fp16 normed activation for a parallel consumer (DeepSeek V3.2 sparse indexer ``wk_weights_proj``). """ aiter_ar = rocm_aiter_ops.get_aiter_allreduce() assert aiter_ar is not None, "aiter allreduce must be initialized" ca = aiter_ar.aiter_ca use_1stage = aiter_ar.use_1stage_fused_ar_rms(input_) result = ca.fused_ar_rms_per_group_quant( input_, residual, w=weight, eps=epsilon, group_size=group_size, registered=torch.cuda.is_current_stream_capturing() and ca.enable_register_for_capturing, use_1stage=use_1stage, emit_bf16=True, ) assert result is not None assert len(result) == 4, "emit_bf16=True must return four tensors from aiter" return result[0], result[1], result[2], result[3] def _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_fake( input_: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: hidden_dim = input_.shape[-1] num_groups = hidden_dim // group_size quant_out = torch.empty(input_.shape, dtype=FP8_DTYPE, device=input_.device) residual_out = torch.empty_like(residual) scale_out = torch.empty( input_.shape[:-1] + (num_groups,), dtype=torch.float32, device=input_.device, ) bf16_norm_out = torch.empty_like(input_) return quant_out, residual_out, scale_out, bf16_norm_out def _rocm_aiter_per_tensor_quant_impl( out: torch.Tensor, x: torch.Tensor, scale: torch.Tensor, is_dynamic: bool, ) -> None: from aiter.ops.quant import dynamic_per_tensor_quant, static_per_tensor_quant if is_dynamic: dynamic_per_tensor_quant(out, x, scale) else: static_per_tensor_quant(out, x, scale) def _rocm_aiter_per_token_quant_impl( x: torch.Tensor, quant_dtype: torch.dtype, scale: torch.Tensor | None = None ) -> tuple[torch.Tensor, torch.Tensor]: from aiter.ops.quant import dynamic_per_token_scaled_quant assert quant_dtype in [torch.int8, FP8_DTYPE] out_shape = x.shape out = torch.empty(x.shape, dtype=quant_dtype, device=x.device) if scale is None: scale = torch.empty((*out_shape[:-1], 1), dtype=torch.float32, device=x.device) dynamic_per_token_scaled_quant( out, x, scale, scale_ub=None, shuffle_scale=False, num_rows=None, num_rows_factor=1, ) return out, scale def _rocm_aiter_per_token_quant_fake( x: torch.Tensor, quant_dtype: torch.dtype, scale: torch.Tensor | None = None ) -> tuple[torch.Tensor, torch.Tensor]: out_shape = x.shape return ( torch.empty(x.shape, dtype=quant_dtype, device=x.device), torch.empty((*out_shape[:-1], 1), dtype=torch.float32, device=x.device), ) def _rocm_aiter_rmsnorm_with_add_fp8_group_quant_impl( x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, variance_epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: from aiter.ops.triton.fused_fp8_quant import fused_rms_fp8_group_quant (x_quant, x_quant_scales), _, _, res = fused_rms_fp8_group_quant( x, weight, variance_epsilon, None, None, None, group_size=group_size, dtype_quant=FP8_DTYPE, res1=residual, ) return ( x_quant, res, x_quant_scales, ) def _rocm_aiter_rmsnorm_with_add_fp8_group_quant_fake( x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, variance_epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: M, N = x.shape scale_shape = (M, (N + group_size - 1) // group_size) return ( torch.empty_like(x, dtype=FP8_DTYPE, device=x.device), torch.empty_like(residual, device=residual.device), torch.empty(scale_shape, dtype=torch.float32, device=x.device), ) def _rocm_aiter_rmsnorm_fp8_group_quant_impl( x: torch.Tensor, weight: torch.Tensor, variance_epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor]: from aiter.ops.triton.fused_fp8_quant import fused_rms_fp8_group_quant (x_quant, x_quant_scales), _, _, res = fused_rms_fp8_group_quant( x, weight, variance_epsilon, None, None, None, group_size=group_size, dtype_quant=FP8_DTYPE, res1=None, ) return (x_quant, x_quant_scales) def _rocm_aiter_rmsnorm_fp8_group_quant_fake( x: torch.Tensor, weight: torch.Tensor, variance_epsilon: float, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor]: M, N = x.shape scale_shape = (M, (N + group_size - 1) // group_size) return ( torch.empty_like(x, dtype=FP8_DTYPE, device=x.device), torch.empty(scale_shape, dtype=torch.float32, device=x.device), ) def _rocm_aiter_fused_rms_gated_fp8_group_quant_impl( x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None, z: torch.Tensor, eps: float, norm_before_gate: bool, activation: str, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor]: """Fused gated-RMSNorm + FP8 group quantization via aiter Triton kernel.""" from aiter.ops.triton.quant import fused_rms_gated_fp8_group_quant return fused_rms_gated_fp8_group_quant( x, weight, bias, z, eps, norm_before_gate=norm_before_gate, activation=activation, out_dtype=FP8_DTYPE, group_size=group_size, ) def _rocm_aiter_fused_rms_gated_fp8_group_quant_fake( x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None, z: torch.Tensor, eps: float, norm_before_gate: bool, activation: str, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor]: M, N = x.shape scale_shape = (M, (N + group_size - 1) // group_size) return ( torch.empty_like(x, dtype=FP8_DTYPE, device=x.device), torch.empty(scale_shape, dtype=torch.float32, device=x.device), ) def _rocm_aiter_group_fp8_quant_impl( x: torch.Tensor, group_size: int, transpose_scale: bool = False, ) -> tuple[torch.Tensor, torch.Tensor]: assert x.shape[-1] % group_size == 0, "Input shape must be divisible by group size" from aiter import QuantType, get_hip_quant aiter_per1x128_quant = get_hip_quant(QuantType.per_1x128) return aiter_per1x128_quant( x.contiguous(), quant_dtype=FP8_DTYPE, transpose_scale=transpose_scale ) def _rocm_aiter_group_fp8_quant_fake( x: torch.Tensor, group_size: int, transpose_scale: bool = False, ) -> tuple[torch.Tensor, torch.Tensor]: M, N = x.shape x_fp8 = torch.empty((M, N), dtype=FP8_DTYPE, device=x.device) out_bs = torch.empty( ( M, (N + group_size - 1) // group_size, ), dtype=torch.float32, device=x.device, ) return x_fp8, out_bs def _rocm_aiter_act_mul_and_fp8_group_quant_impl( x: torch.Tensor, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor]: from aiter.ops.triton.activation import act_mul_and_fp8_group_quant return act_mul_and_fp8_group_quant( x, activation="silu", group_size=group_size, dtype_quant=FP8_DTYPE, ) def _rocm_aiter_act_mul_and_fp8_group_quant_fake( x: torch.Tensor, group_size: int, ) -> tuple[torch.Tensor, torch.Tensor]: M, N = x.shape assert N % 2 == 0 N_half = N // 2 x_fp8 = torch.empty((M, N_half), dtype=FP8_DTYPE, device=x.device) out_bs = torch.empty( ( M, (N_half + group_size - 1) // group_size, ), dtype=torch.float32, device=x.device, ) return x_fp8, out_bs def _rocm_aiter_triton_add_rmsnorm_pad_impl( x: torch.Tensor, weight: torch.Tensor, variance_epsilon: float, residual: torch.Tensor, x_pad_to_multiple: int, ) -> tuple[torch.Tensor, torch.Tensor]: from aiter.ops.triton.fused_add_rmsnorm_pad import fused_add_rmsnorm_pad return fused_add_rmsnorm_pad( x, weight, variance_epsilon, residual, x_pad_to_multiple=x_pad_to_multiple, ) def _rocm_aiter_triton_add_rmsnorm_pad_fake( x: torch.Tensor, weight: torch.Tensor, variance_epsilon: float, residual: torch.Tensor, x_pad_to_multiple: int, ) -> tuple[torch.Tensor, torch.Tensor]: M, N = x.shape if x_pad_to_multiple > 0: N_out = (N + x_pad_to_multiple - 1) // x_pad_to_multiple * x_pad_to_multiple else: N_out = N out = torch.empty((M, N_out), dtype=x.dtype, device=x.device) residual_out = torch.empty_like(residual) return out, residual_out def _fused_mla_dual_rms_norm_impl( x1: torch.Tensor, x1_weight: torch.Tensor, x2: torch.Tensor, x2_weight: torch.Tensor, x1_epsilon: float, x2_epsilon: float, ) -> tuple[torch.Tensor, torch.Tensor]: from aiter.ops.fused_qk_norm_rope_cache_quant import _fused_qk_rmsnorm return _fused_qk_rmsnorm( q_out=None, q=x1, q_weight=x1_weight, q_eps=x1_epsilon, k_out=None, k=x2, k_weight=x2_weight, k_eps=x2_epsilon, ) def _fused_mla_dual_rms_norm_fake( x1: torch.Tensor, x1_weight: torch.Tensor, x2: torch.Tensor, x2_weight: torch.Tensor, x1_epsilon: float, x2_epsilon: float, ) -> tuple[torch.Tensor, torch.Tensor]: return (torch.empty_like(x1), torch.empty_like(x2)) def _fused_mla_dual_rms_norm_per_token_quant_impl( q: torch.Tensor, q_weight: torch.Tensor, kv: torch.Tensor, kv_weight: torch.Tensor, q_epsilon: float, kv_epsilon: float, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Fused MLA q/kv RMSNorm (+ FP8 per-token quant on q) via AITER. Backs the ``fused_mla_dual_rms_norm_per_token_quant`` custom op used by the MLA FP8 attention fusion when the q latent is quantized *per token* (a single ``(M, 1)`` scale). Only the *q* latent is FP8 quantized (it feeds the FP8 ``q_b_proj`` GEMM); the *kv* latent is RMS-normed and consumed by attention as bf16. """ from aiter.ops.fused_qk_rmsnorm_group_quant import ( fused_qk_rmsnorm_per_token_quant, ) mq, nq = q.shape q_out = torch.empty((mq, nq), dtype=FP8_DTYPE, device=q.device) q_scale = torch.empty((mq, 1), dtype=torch.float32, device=q.device) kv_normed = torch.empty(kv.shape, dtype=kv.dtype, device=kv.device) # q -> RMSNorm + FP8 per-token quant (q slot); kv -> RMSNorm only (k slot). # `split` views are accepted directly (unit inner stride); the kernel # handles strided inputs, matching the aiter op-test usage. fused_qk_rmsnorm_per_token_quant( q_out_quantized=q_out, q_out_scale=q_scale, q=q, q_weight=q_weight, q_epsilon=q_epsilon, k_out=kv_normed, k=kv, k_weight=kv_weight, k_epsilon=kv_epsilon, gemma_norm=False, ) return q_out, q_scale, kv_normed def _fused_mla_dual_rms_norm_per_token_quant_fake( q: torch.Tensor, q_weight: torch.Tensor, kv: torch.Tensor, kv_weight: torch.Tensor, q_epsilon: float, kv_epsilon: float, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: mq, nq = q.shape q_out = torch.empty((mq, nq), dtype=FP8_DTYPE, device=q.device) q_scale = torch.empty((mq, 1), dtype=torch.float32, device=q.device) kv_normed = torch.empty(kv.shape, dtype=kv.dtype, device=kv.device) return q_out, q_scale, kv_normed def _fused_mla_dual_rms_norm_group_quant_impl( q: torch.Tensor, q_weight: torch.Tensor, kv: torch.Tensor, kv_weight: torch.Tensor, q_epsilon: float, kv_epsilon: float, group_size: int, transpose_scale: bool, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Fused MLA q/kv RMSNorm (+ FP8 per-1x128 group quant on q) via AITER. Backs the ``fused_mla_dual_rms_norm_group_quant`` custom op used by the DeepSeek-V4 ROCm attention path when the q latent is group-quantized (``(M, N/128)`` scales) for the block-scaled FP8 wq_b GEMMs. The q latent is quantized directly from the fp32 norm accumulator, skipping the bf16 intermediate the standalone quant path would re-read; the kv latent is RMS-normed to bf16 and consumed by the fused insert kernel unchanged. """ from aiter.ops.fused_qk_rmsnorm_group_quant import ( fused_qk_rmsnorm_group_quant, ) mq, nq = q.shape q_out = torch.empty((mq, nq), dtype=FP8_DTYPE, device=q.device) q_scale = torch.empty((mq, nq // group_size), dtype=torch.float32, device=q.device) kv_normed = torch.empty(kv.shape, dtype=kv.dtype, device=kv.device) # q -> RMSNorm + FP8 group quant (q slot); kv -> RMSNorm only (k slot). # `split` views are accepted directly (unit inner stride); the kernel # handles strided inputs, matching the aiter op-test usage. fused_qk_rmsnorm_group_quant( q_out_quantized=q_out, q_out_scale=q_scale, q=q, q_weight=q_weight, q_epsilon=q_epsilon, k_out=kv_normed, k=kv, k_weight=kv_weight, k_epsilon=kv_epsilon, group_size=group_size, transpose_scale=transpose_scale, ) return q_out, q_scale, kv_normed def _fused_mla_dual_rms_norm_group_quant_fake( q: torch.Tensor, q_weight: torch.Tensor, kv: torch.Tensor, kv_weight: torch.Tensor, q_epsilon: float, kv_epsilon: float, group_size: int, transpose_scale: bool, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: mq, nq = q.shape q_out = torch.empty((mq, nq), dtype=FP8_DTYPE, device=q.device) q_scale = torch.empty((mq, nq // group_size), dtype=torch.float32, device=q.device) kv_normed = torch.empty(kv.shape, dtype=kv.dtype, device=kv.device) return q_out, q_scale, kv_normed def _rocm_aiter_gemm_a8wfp4_impl( x: torch.Tensor, w: torch.Tensor, x_scales: torch.Tensor, w_scales: torch.Tensor, out_dtype: torch.dtype, ) -> torch.Tensor: from aiter.ops.triton.gemm_a8wfp4 import gemm_a8wfp4 M, N = x.shape[0], w.shape[0] y = torch.empty(M, N, dtype=out_dtype, device=x.device) gemm_a8wfp4( x=x, w=w, y=y, x_scales=x_scales, w_scales=w_scales, dtype=out_dtype, config=None, ) return y def _rocm_aiter_gemm_a8wfp4_fake( x: torch.Tensor, w: torch.Tensor, x_scales: torch.Tensor, w_scales: torch.Tensor, out_dtype: torch.dtype, ) -> torch.Tensor: return torch.empty(x.shape[0], w.shape[0], dtype=out_dtype, device=x.device) def _triton_rotary_embedding_impl( positions: torch.Tensor, query: torch.Tensor, key: torch.Tensor, head_size: int, cos_sin_cache: torch.Tensor, is_neox: bool, offsets: torch.Tensor | None = None, ) -> None: # Modifies query and key in-place from aiter.ops.triton.rope.rope import ( rope_cached_thd_positions_offsets_2c_fwd_inplace, ) num_tokens = positions.numel() cos, sin = cos_sin_cache.chunk(2, dim=-1) query_shape = query.shape key_shape = key.shape rotate_style = 0 if is_neox else 1 rotary_dim = head_size query = query.view(num_tokens, -1, head_size) key = key.view(num_tokens, -1, head_size) query_ = query[..., :rotary_dim] key_ = key[..., :rotary_dim] positions = positions.view(*query.shape[:1]) rope_cached_thd_positions_offsets_2c_fwd_inplace( query_, key_, cos, sin, positions, offsets, rotate_style, reuse_freqs_front_part=True, nope_first=False, ) query = query.view(query_shape) key = key.view(key_shape) def _rocm_aiter_fp8_attn_impl( q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_descale: torch.Tensor, k_descale: torch.Tensor, v_descale: torch.Tensor, batch_size: int, output_dtype: torch.dtype, scale: float | None = None, cu_seqlens: torch.Tensor | None = None, max_seqlen: torch.Tensor | None = None, ) -> torch.Tensor: """Run AITER FP8 attention for fixed or packed inputs.""" from aiter import flash_attn_varlen_fp8_pertensor_func q_len = q.size(1) if cu_seqlens is None: cu_seqlens = torch.arange( 0, (batch_size + 1) * q_len, step=q_len, dtype=torch.int32, device=q.device ) max_seqlen_value = q_len if max_seqlen is None else max_seqlen.item() q, k, v = (x.flatten(0, 1) for x in (q, k, v)) output = flash_attn_varlen_fp8_pertensor_func( q, k, v, q_descale=q_descale, k_descale=k_descale, v_descale=v_descale, cu_seqlens_q=cu_seqlens, cu_seqlens_k=cu_seqlens, max_seqlen_q=max_seqlen_value, max_seqlen_k=max_seqlen_value, causal=False, softmax_scale=scale, ) return output.to(output_dtype).reshape(batch_size, q_len, *output.shape[1:]) def _rocm_aiter_fp8_attn_fake( q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_descale: torch.Tensor, k_descale: torch.Tensor, v_descale: torch.Tensor, batch_size: int, output_dtype: torch.dtype, scale: float | None = None, cu_seqlens: torch.Tensor | None = None, max_seqlen: torch.Tensor | None = None, ) -> torch.Tensor: return torch.empty( (*q.shape[:-1], v.shape[-1]), device=q.device, dtype=output_dtype ) def _mhc_delayed_pre_tail( gemm_out: torch.Tensor, sqrsum: torch.Tensor, residual: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, pre_mix: torch.Tensor | None, rms_eps: float, hc_pre_eps: float, hc_sinkhorn_eps: float, hc_post_mult_value: float, sinkhorn_repeat: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Finish a delayed mHC pre once AITER has produced the projection. Shared by the plain and the post-fused entry points, which differ only in which kernel computes ``gemm_out`` / ``sqrsum`` and the residual they project. ``mhc_pre_big_fuse`` also writes a collapse against its own pre-mix, which the delayed formulation cannot use and so discards. Returns ``(post_mix, comb_mix, layer_input, next_pre_mix)``. """ from aiter.ops.mhc import mhc_pre_big_fuse num_tokens, hc_mult, hidden_size = residual.shape device = residual.device with torch.device(device): post_mix = torch.empty( num_tokens, hc_mult, 1, dtype=torch.float32, device=device ) comb_mix = torch.empty( num_tokens, hc_mult, hc_mult, dtype=torch.float32, device=device ) unused_collapse = torch.empty( num_tokens, hidden_size, dtype=torch.bfloat16, device=device ) mhc_pre_big_fuse( post_mix, comb_mix, unused_collapse, gemm_out, sqrsum, hc_scale, hc_base, residual, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_mult_value, sinkhorn_repeat, ) next_pre_mix = torch.ops.vllm.mhc_pre_mix_triton( gemm_out, sqrsum, hc_scale, hc_base, hc_mult, hc_mult * hidden_size, rms_eps, hc_pre_eps, ) if pre_mix is None: # Model entry selects residual stream zero. layer_input = residual[:, 0] else: layer_input = torch.ops.vllm.hc_collapse_triton(residual, pre_mix) return post_mix, comb_mix, layer_input, next_pre_mix # Global flag to ensure ops are registered only once _OPS_REGISTERED = False _AITER_SITUV2_ACT_ENV = { "a8w4": "AITER_SITUV2_A8W4", "a4w4": "AITER_SITUV2_A4W4", } def _resolve_situv2_activation() -> str: """VLLM_ROCM_USE_AITER_MOE_SITUV2 -> a4w4 | a8w4 | a16w4. auto (default) and the legacy 1 mean a4w4; legacy 0 means a16w4. """ value = envs.VLLM_ROCM_USE_AITER_MOE_SITUV2.lower() if value in ("auto", "1"): return "a4w4" if value == "0": return "a16w4" return value def _sync_aiter_situv2_moe_env() -> None: """Set the AITER_SITUV2_* env matching VLLM_ROCM_USE_AITER_MOE_SITUV2. AITER reads AITER_SITUV2_A8W4 / AITER_SITUV2_A4W4 and defaults to a16w4, so exactly one is set for a8w4/a4w4 and both are cleared for a16w4. """ import os selected = _AITER_SITUV2_ACT_ENV.get(_resolve_situv2_activation()) for name in _AITER_SITUV2_ACT_ENV.values(): if name == selected: os.environ[name] = "1" else: os.environ.pop(name, None) _GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN = 64 * 1024 _GFX950_C4A_NATIVE_MAX_ROWS = 256 _GFX950_DSV4_NATIVE_MAX_COLUMNS = 1024 * 1024 class rocm_aiter_ops: """ROCm AITER operations wrapper for AMD GPU acceleration in vLLM. This class centralizes the import and registration of AITER ops, and provides a unified interface for checking if AITER is enabled. Operations are only available on supported gfx9 architectures when aiter is installed. The class uses environment variables to control which features are enabled, allowing fine-grained control over which AITER optimizations are used. Environment Variables: VLLM_ROCM_USE_AITER: Main toggle for all AITER operations. VLLM_ROCM_USE_AITER_LINEAR: Controls GEMM and quantization ops. VLLM_ROCM_USE_AITER_RMSNORM: Controls RMSNorm operations. VLLM_ROCM_USE_AITER_MOE: Controls MoE (Mixture of Experts) ops. VLLM_ROCM_USE_AITER_MLA: Controls MLA (Multi-head Latent Attention) ops. VLLM_ROCM_USE_AITER_MHA: Controls MHA ops including flash_attn_varlen. VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION: Controls Triton unified attention. VLLM_ROCM_USE_AITER_FP8BMM: Controls FP8 batched matrix multiply. VLLM_ROCM_USE_AITER_FP4_ASM_GEMM: Controls FP4 assembly GEMM. VLLM_ROCM_USE_AITER_TRITON_ROPE: Controls Triton rotary embeddings. VLLM_ROCM_USE_AITER_TRITON_SPARSE_MLA: Controls Triton sparse MLA (gfx950). VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: Controls shared expert fusion. VLLM_ROCM_USE_AITER_MOE_SITUV2: SiTUv2 FlyDSL MoE activation dtype (a16w4 | a8w4 | a4w4). VLLM_ROCM_USE_AITER_TRITON_GEMM: Controls Triton unquantized GEMM. Note: The environment variables are assigned when the module is imported, so you can't change the environment variables after the module is imported. This is done out of performance consideration. Accessing environment variables is expensive as described in issue https://github.com/vllm-project/vllm/issues/17067 so we don't want to do it repeatedly, especially in the hot path (the forward pass). You can call the refresh_env_variables() function to reload the env variables after monkey patching the env variables in the unit test. Check Functions: All check functions (is_*_enabled) are decorated with @if_aiter_supported, which verifies: (1) platform is ROCm, (2) device arch is gfx9, and (3) aiter library is installed. The check function then also verifies the corresponding environment variable is enabled. i.e. ___ is_enabled() == current_platform.is_rocm() and | checked by current_platform.is_on_gfx9() and | @if_aiter_supported IS_AITER_FOUND and _______________| cls._AITER_ENABLED -----> Check by the logic in `is_enabled()` Example: from vllm._aiter_ops import rocm_aiter_ops # Check if aiter is enabled before using operations if rocm_aiter_ops.is_enabled(): result = rocm_aiter_ops.per_token_quant(x, FP8_DTYPE) Operations: - GEMM operations: gemm_a8w8, gemm_a8w8_blockscale - Fused MoE: fused_moe, asm_moe_tkw1 - Routing: topk_softmax, biased_grouped_topk, grouped_topk - MLA decode: mla_decode_fwd - Quantization: per_tensor_quant, per_token_quant, group_fp8_quant - Triton ops: triton_rotary_embed, triton_fp8_bmm, triton_gemm_a8w8_blockscale """ _MOE_DISPATCH_POLICY: int | None = None @classmethod @if_aiter_supported def get_moe_dispatch_policy(cls) -> int: """Cached MoE sorting dispatch policy.""" if cls._MOE_DISPATCH_POLICY is None: import vllm.envs as envs cls._MOE_DISPATCH_POLICY = envs.VLLM_ROCM_AITER_MOE_DISPATCH_POLICY return cls._MOE_DISPATCH_POLICY # Check if the env variable is set _AITER_ENABLED = envs.VLLM_ROCM_USE_AITER # Chunk length the AITER FlyDSL K5 prefill kernels are compiled for. GDN_FLYDSL_CHUNK_SIZE = 64 _CUSTOM_ALL_REDUCE_ENABLED = envs.VLLM_ROCM_USE_AITER_CUSTOM_AR _LINEAR_ENABLED = envs.VLLM_ROCM_USE_AITER_LINEAR _FMOE_ENABLED = envs.VLLM_ROCM_USE_AITER_MOE _MLA_ENABLED = envs.VLLM_ROCM_USE_AITER_MLA _MHA_ENABLED = envs.VLLM_ROCM_USE_AITER_MHA _SHUFFLE_KV_CACHE_ENABLED = envs.VLLM_ROCM_SHUFFLE_KV_CACHE_LAYOUT _TRITON_UNIFIED_ATTN_ENABLED = envs.VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION # TODO: Consolidate under _LINEAR_ENABLED _FP8BMM_ENABLED = envs.VLLM_ROCM_USE_AITER_FP8BMM _FP4BMM_ENABLED = envs.VLLM_ROCM_USE_AITER_FP4BMM _LINEAR_HIPBMM_ENABLED = envs.VLLM_ROCM_USE_AITER_LINEAR_HIPBMM # TODO: Consolidate under _LINEAR_ENABLED _FP4_GEMM_DYNAMIC_QUANT_ASM = envs.VLLM_ROCM_USE_AITER_FP4_ASM_GEMM # TODO: Consolidate under VLLM_ROCM_USE_AITER_ROPE _TRITON_ROTARY_EMBED = envs.VLLM_ROCM_USE_AITER_TRITON_ROPE _TRITON_SPARSE_MLA = envs.VLLM_ROCM_USE_AITER_TRITON_SPARSE_MLA _MOE_SHARED_EXPERTS_ENABLED = envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS _MOE_SITUV2 = _resolve_situv2_activation() # TODO: Consolidate under _LINEAR_ENABLED _TRITON_UNQUANT_GEMM = envs.VLLM_ROCM_USE_AITER_TRITON_GEMM # Lazily probed: whether aiter.topk_softmax supports the # num_shared_experts / shared_expert_scoring_func args (7-arg form). _TOPK_SOFTMAX_FUSED_SIGMOID: bool | None = None @classmethod def refresh_env_variables(cls): """Since the environment variables are assigned when the module is imported, This is a helper function to reload all the env variables from the environment variables. for example, after monkey patching the env variables in the unit test, you can call this function to reload the env variables. """ cls._AITER_ENABLED = envs.VLLM_ROCM_USE_AITER cls._CUSTOM_ALL_REDUCE_ENABLED = envs.VLLM_ROCM_USE_AITER_CUSTOM_AR cls._LINEAR_ENABLED = envs.VLLM_ROCM_USE_AITER_LINEAR cls._FMOE_ENABLED = envs.VLLM_ROCM_USE_AITER_MOE cls._MLA_ENABLED = envs.VLLM_ROCM_USE_AITER_MLA cls._MHA_ENABLED = envs.VLLM_ROCM_USE_AITER_MHA cls._SHUFFLE_KV_CACHE_ENABLED = envs.VLLM_ROCM_SHUFFLE_KV_CACHE_LAYOUT cls._TRITON_UNIFIED_ATTN_ENABLED = envs.VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION cls._FP8BMM_ENABLED = envs.VLLM_ROCM_USE_AITER_FP8BMM cls._FP4BMM_ENABLED = envs.VLLM_ROCM_USE_AITER_FP4BMM cls._LINEAR_HIPBMM_ENABLED = envs.VLLM_ROCM_USE_AITER_LINEAR_HIPBMM cls._FP4_GEMM_DYNAMIC_QUANT_ASM = envs.VLLM_ROCM_USE_AITER_FP4_ASM_GEMM cls._TRITON_ROTARY_EMBED = envs.VLLM_ROCM_USE_AITER_TRITON_ROPE cls._TRITON_SPARSE_MLA = envs.VLLM_ROCM_USE_AITER_TRITON_SPARSE_MLA cls._MOE_SHARED_EXPERTS_ENABLED = envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS cls._MOE_SITUV2 = _resolve_situv2_activation() _sync_aiter_situv2_moe_env() cls._TRITON_UNQUANT_GEMM = envs.VLLM_ROCM_USE_AITER_TRITON_GEMM cls._MOE_DISPATCH_POLICY = envs.VLLM_ROCM_AITER_MOE_DISPATCH_POLICY @staticmethod def get_aiter_activation_type(activation_str: str) -> "ActivationType | None": """Given an activation type as a string, returns the corresponding aiter ActivationType enum. Supported activation types: "no", "none", "silu", "gelu", "swiglu". Returns None if the mapping fails. Args: activation_str (str): Activation type as string. Returns: Aiter ActivationType enum value, or None if not found. """ # Import only locally, since aiter may not always be available. try: from aiter import ActivationType except ImportError: return None if not isinstance(activation_str, str): return None name = activation_str.strip().lower() mapping = { "none": ActivationType.No, "no": ActivationType.No, "silu": ActivationType.Silu, "gelu": ActivationType.Gelu, "swiglu": ActivationType.Swiglu, "situ": getattr(ActivationType, "Situv2", None), } return mapping.get(name) @staticmethod def get_aiter_quant_type(quant_type_str: str) -> "QuantType | None": """Given a quantization type as a string, returns the corresponding aiter QuantType enum. Supported quantization types: "no", "per_tensor", "per_token", "per_1x32", "per_1x128", "per_128x128". Returns None if the mapping fails. Args: quant_type_str (str): Quantization type as string. Returns: Aiter QuantType enum value, or None if not found. """ try: from aiter import QuantType except ImportError: return None if not isinstance(quant_type_str, str): return None name = quant_type_str.strip().lower() mapping = { "no": QuantType.No, "per_tensor": QuantType.per_Tensor, "per_token": QuantType.per_Token, "per_1x32": QuantType.per_1x32, "per_1x128": QuantType.per_1x128, "per_128x128": QuantType.per_128x128, } return mapping.get(name) @classmethod @if_aiter_supported def is_enabled(cls) -> bool: return cls._AITER_ENABLED @classmethod def is_rdna_aiter_enabled(cls) -> bool: """AITER on RDNA4 (gfx12): library present, arch is rdna4, and the user enabled aiter. Only aiter's Triton kernels exist on gfx12 (no CK build), so this deliberately stays off the gfx9/CK `@if_aiter_supported` umbrella and gates only the Triton paths rdna4 uses. The gfx12 analog of `is_enabled()`.""" if not current_platform.is_rocm() or not IS_AITER_FOUND: return False from vllm.platforms.rocm import on_rdna4 return on_rdna4() and cls._AITER_ENABLED @classmethod def is_rdna_linear_enabled(cls) -> bool: """RDNA4 (gfx12) analog of is_linear_enabled() (aiter Triton blockscale).""" return cls.is_rdna_aiter_enabled() and cls._LINEAR_ENABLED @classmethod @if_aiter_supported def is_linear_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._LINEAR_ENABLED @classmethod @if_aiter_supported def is_linear_fp8_enabled(cls) -> bool: return cls.is_linear_enabled() @classmethod @if_aiter_supported def is_fused_moe_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._FMOE_ENABLED @classmethod @if_aiter_supported def is_fusion_moe_shared_experts_enabled(cls) -> bool: return cls.is_fused_moe_enabled() and cls._MOE_SHARED_EXPERTS_ENABLED @classmethod @if_aiter_supported def is_fused_moe_situv2_enabled(cls) -> bool: """True when a low-precision (a8w4/a4w4) SiTUv2 activation is selected.""" return cls.is_fused_moe_enabled() and cls._MOE_SITUV2 != "a16w4" @classmethod @if_aiter_supported def get_fused_moe_situv2_activation(cls) -> str: """SiTUv2 activation dtype name: a16w4, a8w4 or a4w4.""" return cls._MOE_SITUV2 @classmethod @if_aiter_supported def is_fused_moe_situv2_gate_up_interleaved(cls) -> bool: """Whether the SiTUv2 kernels expect gate/up-interleaved w13. The a8w4 FlyDSL stage-1 kernels (_gui_) are interleaved; a16w4 and a4w4 are separated. Weight shuffle (oracle/mxfp4.py) and gate_mode (experts/rocm_aiter_moe.py) must both use this. """ return cls._MOE_SITUV2 == "a8w4" @classmethod @if_aiter_supported def topk_softmax_supports_fused_sigmoid(cls) -> bool: """Check if topk_softmax supports fused shared expert activation.""" if cls._TOPK_SOFTMAX_FUSED_SIGMOID is None: try: import inspect from aiter import topk_softmax params = inspect.signature(topk_softmax).parameters if "num_shared_experts" in params: cls._TOPK_SOFTMAX_FUSED_SIGMOID = True else: # @compile_ops wrapper loses the original signature. # Fall back to the torch custom op schema. import torch schema = getattr( getattr(torch.ops.aiter, "topk_softmax", None), "default", None ) schema_str = str(getattr(schema, "_schema", "")) cls._TOPK_SOFTMAX_FUSED_SIGMOID = "num_shared_experts" in schema_str except (ImportError, ValueError): cls._TOPK_SOFTMAX_FUSED_SIGMOID = False return cls._TOPK_SOFTMAX_FUSED_SIGMOID @classmethod @if_aiter_supported def fuse_sigmoid_in_kernel(cls, aiter_topK_meta_data: object) -> bool: """Whether fused shared-expert sigmoid in the topk kernel is usable. Combines the cached static capability checks (FSE enabled, fused-moe enabled, topk_softmax supports fused sigmoid) with the runtime readiness check (topK meta-data buffer initialized). ``aiter_topK_meta_data`` is accepted as a parameter rather than imported internally so callers cannot hit initialization-order issues where the module-level global has not been set yet. """ return ( cls.is_fusion_moe_shared_experts_enabled() and cls.topk_softmax_supports_fused_sigmoid() and aiter_topK_meta_data is not None ) @classmethod @if_aiter_supported def is_mla_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._MLA_ENABLED @classmethod @if_aiter_supported def mla_decode_supports_non_causal(cls) -> bool: """Whether installed aiter.mla.mla_decode_fwd accepts `causal`. Added in aiter v0.1.20. A missing argument is a hard error rather than a boolean the caller can use to pick another MLA backend. """ import inspect from aiter.mla import mla_decode_fwd if "causal" not in inspect.signature(mla_decode_fwd).parameters: raise RuntimeError( "ROCM_AITER_MLA requires aiter.mla.mla_decode_fwd(..., causal=). " "The installed aiter is causal-only; upgrade aiter rather than " "falling back to another MLA backend." ) return True @classmethod @if_aiter_supported def is_mha_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._MHA_ENABLED @classmethod @if_aiter_supported def is_custom_all_reduce_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._CUSTOM_ALL_REDUCE_ENABLED @classmethod @if_aiter_supported def is_shuffle_kv_cache_enabled(cls) -> bool: return cls._SHUFFLE_KV_CACHE_ENABLED @classmethod @if_aiter_supported def fused_qknorm_idxrqknorm_enabled( cls, kv_cache_dtype: str, k_scale: torch.Tensor | None = None, v_scale: torch.Tensor | None = None, ) -> bool: """Whether MiniMax-M3 can use AITER's consolidated fused QK-norm. Requires AITER to be installed and enabled. bf16 (``auto``) always qualifies; fp8 e4m3 also needs K/V scales. Other cache dtypes fall back to vLLM's fused kernel. """ if not cls._AITER_ENABLED: return False if kv_cache_dtype == "auto": return True if kv_cache_dtype in ("fp8", "fp8_e4m3"): return k_scale is not None and v_scale is not None return False @classmethod def fused_qknorm_idxrqknorm( cls, qkv: torch.Tensor, q_norm_weight: torch.Tensor, k_norm_weight: torch.Tensor, cos_sin_cache: torch.Tensor, positions: torch.Tensor, num_heads: int, num_kv_heads: int, rotary_dim: int, eps: float, slot_mapping: torch.Tensor, kv_cache_k: torch.Tensor, kv_cache_v: torch.Tensor, q_out: torch.Tensor, kv_cache_dtype: str, k_scale: torch.Tensor | None = None, v_scale: torch.Tensor | None = None, index_q_norm_weight: torch.Tensor | None = None, index_k_norm_weight: torch.Tensor | None = None, num_index_heads: int = 0, index_cache: torch.Tensor | None = None, index_q_out: torch.Tensor | None = None, index_slot_mapping: torch.Tensor | None = None, skip_index_branch: bool = False, ) -> None: """Run consolidated MiniMax-M3 QK-norm fusion. Callers must check ``fused_qknorm_idxrqknorm_enabled`` first. Runtime failures from the AITER op are deliberately propagated. """ if kv_cache_dtype == "auto": aiter_kv_cache_dtype = "auto" aiter_k_scale = None aiter_v_scale = None elif kv_cache_dtype in ("fp8", "fp8_e4m3"): aiter_kv_cache_dtype = "fp8_e4m3_static" aiter_k_scale = k_scale aiter_v_scale = v_scale else: raise ValueError( "AITER fused QK-norm requires kv_cache_dtype 'auto', 'fp8', " f"or 'fp8_e4m3', got {kv_cache_dtype!r}" ) aiter_index_cache_dtype = "fp8" if _is_fp8_e4m3_tensor(index_cache) else "auto" from aiter import fused_qknorm_idxrqknorm fused_qknorm_idxrqknorm( qkv, q_norm_weight, k_norm_weight, cos_sin_cache, positions, num_heads, num_kv_heads, rotary_dim, eps, index_q_norm_weight=index_q_norm_weight, index_k_norm_weight=index_k_norm_weight, num_index_heads=num_index_heads, slot_mapping=slot_mapping, kv_cache_k=kv_cache_k, kv_cache_v=kv_cache_v, index_cache=index_cache, block_size=16, q_out=q_out, index_q_out=index_q_out, index_slot_mapping=index_slot_mapping, kv_cache_dtype=aiter_kv_cache_dtype, index_cache_dtype=aiter_index_cache_dtype, k_scale=aiter_k_scale, v_scale=aiter_v_scale, asm_layout=True, skip_index_branch=skip_index_branch, ) @classmethod @if_aiter_supported def is_triton_unified_attn_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._TRITON_UNIFIED_ATTN_ENABLED @classmethod @if_aiter_supported def is_fp8bmm_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._FP8BMM_ENABLED @classmethod @if_aiter_supported def is_fp4bmm_enabled(cls) -> bool: from vllm.platforms.rocm import get_cdna_version # TODO GFX1250: Enable for cdna 4+ when aiter supports batched_gemm_a16wfp4 on gfx1250 return cls._AITER_ENABLED and cls._FP4BMM_ENABLED and get_cdna_version() == 4 @classmethod @if_aiter_supported def is_linear_hipbmm_enabled(cls) -> bool: from vllm.platforms.rocm import get_cdna_version return ( cls.is_linear_enabled() and (get_cdna_version() > 2) and cls._LINEAR_HIPBMM_ENABLED ) @classmethod @if_aiter_supported def is_asm_fp4_gemm_dynamic_quant_enabled(cls) -> bool: from vllm.platforms.rocm import on_gfx950 return cls._AITER_ENABLED and cls._FP4_GEMM_DYNAMIC_QUANT_ASM and on_gfx950() @classmethod @if_aiter_supported def is_triton_rotary_embed_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._TRITON_ROTARY_EMBED @classmethod @if_aiter_supported def is_triton_sparse_mla_enabled(cls) -> bool: if not cls._TRITON_SPARSE_MLA: return False from vllm.platforms.rocm import on_gfx950 if not cls._AITER_ENABLED: reason = "VLLM_ROCM_USE_AITER is off" elif not on_gfx950(): reason = "the kernel is gfx950-only" else: return True logger.warning_once( "VLLM_ROCM_USE_AITER_TRITON_SPARSE_MLA is set, but %s; ignoring it.", reason, ) return False @classmethod @if_aiter_supported def is_triton_gemm_enabled(cls) -> bool: return cls._AITER_ENABLED and cls._TRITON_UNQUANT_GEMM @classmethod @if_aiter_supported def is_tgemm_enabled(cls) -> bool: from vllm.platforms.rocm import on_gfx950 return cls.is_linear_enabled() and on_gfx950() @classmethod def get_aiter_allreduce(cls): """Return the TP device communicator's AITER custom-allreduce if it has one, return None otherwise """ from vllm.distributed.device_communicators.aiter_custom_all_reduce import ( AiterCustomAllreduce, ) from vllm.distributed.parallel_state import get_tp_group device_comm = get_tp_group().device_communicator aiter_ar_comm = getattr(device_comm, "aiter_ar_comm", None) return ( aiter_ar_comm if isinstance(aiter_ar_comm, AiterCustomAllreduce) else None ) @staticmethod def _gdn_triton_kernels_importable() -> bool: try: import aiter.ops.triton.causal_conv1d_update_single_token # noqa: F401 import aiter.ops.triton.gated_delta_net # noqa: F401 from aiter.ops.triton.quant import ( # noqa: F401 fused_rms_gated_fp8_group_quant, ) return True except (ImportError, ModuleNotFoundError): return False @staticmethod def _gdn_flydsl_prefill_kernels_importable() -> bool: try: from aiter.ops.flydsl.linear_attention_prefill_kernels import ( # noqa: F401 chunk_gated_delta_rule_fwd_h_flydsl_opt, gdn_prepare_flydsl_supported, gdn_prepare_fwd_flydsl, ) from aiter.ops.triton.gated_delta_net import ( # noqa: F401 build_gated_delta_rule_prefill_metadata, chunk_gated_delta_rule_opt_vk, ) return True except (ImportError, ModuleNotFoundError): return False @classmethod @if_aiter_supported def are_gdn_triton_kernels_available(cls) -> bool: """Check if AITER Triton kernels for GDN attention are importable. These are optional Triton kernels (conv1d fast-path, gated delta net) used by GatedDeltaNetAttention's decode fast-path. They may be absent in older aiter builds. """ return cls._AITER_ENABLED and cls._gdn_triton_kernels_importable() @classmethod @functools.cache def is_gdn_flydsl_prefill_available(cls) -> bool: """Whether the opt-in AITER FlyDSL GDN prefill path can be used. Selecting the backend is an explicit opt-in in itself, but it still runs AITER kernels, so VLLM_ROCM_USE_AITER remains the one switch that turns all of them off. """ return ( cls._AITER_ENABLED and is_aiter_found_and_supported() and cls._gdn_flydsl_prefill_kernels_importable() ) @classmethod def build_gdn_flydsl_prefill_metadata( cls, seq_lens_cpu: list[int], cu_seqlens: torch.Tensor, ) -> object: """Build the reusable varlen metadata FlyDSL GDN prefill runs against. The K5 recurrence is compiled for a fixed 64-token chunk, so the chunk size is a property of the AITER kernel rather than of FLA, and is not the caller's to choose. """ from aiter.ops.triton.gated_delta_net import ( build_gated_delta_rule_prefill_metadata, ) return build_gated_delta_rule_prefill_metadata( seq_lens_cpu, cu_seqlens=cu_seqlens, chunk_size=cls.GDN_FLYDSL_CHUNK_SIZE, ) @classmethod def gdn_flydsl_prefill( cls, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, g: torch.Tensor, beta: torch.Tensor, initial_state: torch.Tensor, output_final_state: bool, cu_seqlens: torch.Tensor | None, aiter_prefill_metadata: object | None, use_qk_l2norm_in_kernel: bool, ) -> tuple[torch.Tensor, torch.Tensor]: """Run GDN prefill through AITER's VK path with the FlyDSL kernels. Every argument AITER takes is named here rather than forwarded as ``**kwargs``, so a rename or reordering on the AITER side fails at this call instead of silently binding to the wrong parameter. """ from aiter.ops.flydsl.linear_attention_prefill_kernels import ( gdn_prepare_flydsl_supported, ) from aiter.ops.triton.gated_delta_net import chunk_gated_delta_rule_opt_vk if cu_seqlens is not None and aiter_prefill_metadata is None: raise RuntimeError( "AITER FlyDSL GDN prefill requires reusable varlen prefill metadata." ) if not gdn_prepare_flydsl_supported(k, v): raise RuntimeError( "AITER FlyDSL GDN prepare does not support the runtime input shape, " "dtype, or device." ) logger.info_once( "Dispatching AITER FlyDSL GDN prefill (prepare=flydsl, chunk=flydsl)." ) return chunk_gated_delta_rule_opt_vk( q=q, k=k, v=v, g=g, beta=beta, initial_state=initial_state, output_final_state=output_final_state, cu_seqlens=cu_seqlens, use_chunk_flydsl=True, use_prepare_flydsl=True, state_dtype=initial_state.dtype, prefill_metadata=aiter_prefill_metadata, # The caller stages the state densely and writes it back, so the # kernel gets a plain [N, H, V, K] buffer and never an index into # the pool; there is correspondingly nothing to update in place. inplace_final_state=False, use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel, ) @classmethod def is_rdna_gdn_triton_kernels_available(cls) -> bool: """RDNA4 (gfx12) analog of are_gdn_triton_kernels_available().""" return cls.is_rdna_aiter_enabled() and cls._gdn_triton_kernels_importable() @classmethod @if_aiter_supported @functools.cache def fused_moe_supports_gate_mode(cls) -> bool: """Probe whether the installed aiter.fused_moe accepts `gate_mode`. Added in https://github.com/ROCm/aiter/pull/3123 (>=0.1.14). Builds with older AITER must omit this argument. """ import inspect from aiter.fused_moe import fused_moe return "gate_mode" in inspect.signature(fused_moe).parameters @classmethod @if_aiter_supported @functools.cache def fused_moe_supports_quant_dtype_a(cls) -> bool: """Probe whether the installed aiter.fused_moe accepts `quant_dtype_a`. Added in https://github.com/ROCm/aiter/pull/5439 (unreleased at merge time). Older AITER can't override the activation quant dtype. """ import inspect from aiter.fused_moe import fused_moe return "quant_dtype_a" in inspect.signature(fused_moe).parameters @staticmethod def _probe_dsv4_i384_fhmoe_capability(num_tokens: int) -> bool: """Probe AITER's CSV-backed DSV4 native-I384 FHMoE contract.""" if type(num_tokens) is not int or num_tokens <= 0: return False try: import inspect import aiter.fhmoe as fhmoe from aiter.fused_moe import fused_moe params = inspect.signature(fused_moe).parameters supports_dsv4_i384_fhmoe = getattr(fhmoe, "supports_dsv4_i384_fhmoe", None) if not callable(supports_dsv4_i384_fhmoe): return False supports_num_tokens = supports_dsv4_i384_fhmoe(num_tokens) except Exception: return False return supports_num_tokens is True and all( name in params for name in ( "shared_w1", "shared_w2", "shared_w1_scale", "shared_w2_scale", "shared_expert_id", ) ) @classmethod @if_aiter_supported @functools.cache def fused_moe_supports_heterogeneous_shared_expert(cls, num_tokens: int) -> bool: """Whether AITER has DSV4 native-I384 configs through the given M.""" return cls._probe_dsv4_i384_fhmoe_capability(num_tokens) @classmethod @if_aiter_supported def is_indexer_top_k_enabled(cls) -> bool: """gfx950 is the only arch with tuned AITER indexer top-k kernels.""" from vllm.platforms.rocm import on_gfx950 return cls._AITER_ENABLED and on_gfx950() @classmethod def is_indexer_top_k_supported( cls, *, is_prefill: bool, compress_ratio: int, num_rows: int, max_valid_seq_len: int | None = None, ) -> bool: """Whether AITER's sparse indexer top-k beats the in-tree kernel for this shape.""" if compress_ratio <= 1 or not cls.is_indexer_top_k_enabled(): return False if not is_prefill: assert max_valid_seq_len is not None # AITER v0.1.19 decode is one-block only. This measured gfx950 # FP32/k=1024 compressed-row boundary is independent of the native # split-count boundary in sampler.cu. if ( num_rows <= _GFX950_C4A_NATIVE_MAX_ROWS and max_valid_seq_len > _GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN ): return False return True @staticmethod def dsv4_indexer_prefers_native_top_k( *, num_rows: int, num_columns: int, topk_tokens: int, ) -> bool: """The in-tree decode kernel's measured advantage window over AITER. Tuned on DSV4's compressed-KV logits, so it applies to that indexer only.""" return ( topk_tokens == 512 and 0 < num_rows <= 384 and num_columns <= _GFX950_DSV4_NATIVE_MAX_COLUMNS ) @staticmethod def indexer_top_k_decode( logits: torch.Tensor, next_n: int, seq_lens: torch.Tensor, topk_indices: torch.Tensor, topk_tokens: int, ) -> None: from aiter.ops.topk import top_k_per_row_decode top_k_per_row_decode( logits, next_n, seq_lens, topk_indices, logits.shape[0], logits.stride(0), logits.stride(1), k=topk_tokens, ) @staticmethod def indexer_top_k_prefill( logits: torch.Tensor, row_starts: torch.Tensor, row_ends: torch.Tensor, indices: torch.Tensor, topk_tokens: int, ) -> None: from aiter.ops.topk import top_k_per_row_prefill top_k_per_row_prefill( logits, row_starts, row_ends, indices, None, logits.shape[0], logits.stride(0), logits.stride(1), k=topk_tokens, ) @staticmethod def register_ops_once() -> None: global _OPS_REGISTERED if not ( is_aiter_found_and_supported() or is_aiter_found_and_supported_on_rdna4() ): if not current_platform.is_rocm(): return from vllm.platforms.rocm import on_gfx11, on_gfx950 # This op has self-contained Triton/C++ implementations on gfx11 # and gfx950. Only its optional top-k fast path comes from aiter. if (on_gfx11() or on_gfx950()) and not _OPS_REGISTERED: direct_register_custom_op( op_name="rocm_aiter_sparse_attn_indexer", op_func=rocm_aiter_sparse_attn_indexer, mutates_args=["topk_indices_buffer", "candidate_blocks"], fake_impl=rocm_aiter_sparse_attn_indexer_fake, dispatch_key=current_platform.dispatch_key, ) _OPS_REGISTERED = True return if not _OPS_REGISTERED: # register all the custom ops here direct_register_custom_op( op_name="rocm_aiter_asm_moe_tkw1", op_func=_rocm_aiter_asm_moe_tkw1_impl, mutates_args=[], fake_impl=_rocm_aiter_asm_moe_tkw1_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_fused_moe", op_func=_rocm_aiter_fused_moe_impl, mutates_args=[], fake_impl=_rocm_aiter_fused_moe_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_topk_softmax", op_func=_rocm_aiter_topk_softmax_impl, mutates_args=["topk_weights", "topk_indices", "token_expert_indices"], dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_topk_sigmoid", op_func=_rocm_aiter_topk_sigmoid_impl, mutates_args=["topk_weights", "topk_indices"], dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_biased_grouped_topk", op_func=_rocm_aiter_biased_grouped_topk_impl, mutates_args=["topk_weights", "topk_ids"], dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_grouped_topk", op_func=_rocm_aiter_grouped_topk_impl, mutates_args=["topk_weights", "topk_ids"], dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_fused_topk", op_func=_rocm_aiter_fused_topk_impl, mutates_args=[], fake_impl=_rocm_aiter_fused_topk_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_gather_kv_b_proj", op_func=_rocm_aiter_gather_kv_b_proj_impl, mutates_args=["k", "v"], fake_impl=_rocm_aiter_gather_kv_b_proj_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_mla_decode_fwd", op_func=_rocm_aiter_mla_decode_fwd_impl, mutates_args=["o"], ) direct_register_custom_op( op_name="rocm_aiter_mla_decode_fwd_lse", op_func=_rocm_aiter_mla_decode_fwd_lse_impl, mutates_args=["o"], fake_impl=_rocm_aiter_mla_decode_fwd_lse_fake, ) direct_register_custom_op( op_name="rocm_aiter_w8a8_gemm", op_func=_rocm_aiter_w8a8_gemm_impl, fake_impl=_rocm_aiter_w8a8_gemm_fake, ) direct_register_custom_op( op_name="_rocm_aiter_preshuffled_per_token_w8a8_gemm", op_func=_rocm_aiter_preshuffled_per_token_w8a8_gemm_impl, fake_impl=_rocm_aiter_preshuffled_per_token_w8a8_gemm_fake, ) direct_register_custom_op( op_name="rocm_aiter_hipb_mm_fp8", op_func=_rocm_aiter_hipb_mm_fp8_impl, fake_impl=_rocm_aiter_hipb_mm_fp8_fake, ) direct_register_custom_op( op_name="rocm_aiter_triton_gemm_a8w8_blockscale", op_func=_rocm_aiter_triton_gemm_a8w8_blockscale_impl, fake_impl=_rocm_aiter_triton_gemm_a8w8_blockscale_fake, ) direct_register_custom_op( op_name="rocm_aiter_gemm_a8w8_blockscale", op_func=_rocm_aiter_gemm_a8w8_blockscale_impl, fake_impl=_rocm_aiter_gemm_a8w8_blockscale_fake, ) direct_register_custom_op( op_name="rocm_aiter_gemm_a8w8_blockscale_bpreshuffle", op_func=_rocm_aiter_gemm_a8w8_blockscale_bpreshuffle_impl, fake_impl=_rocm_aiter_gemm_a8w8_blockscale_bpreshuffle_fake, ) direct_register_custom_op( op_name="rocm_aiter_rmsnorm_fused_dynamic_quant", op_func=_rocm_aiter_rmsnorm_fused_dynamic_quant_impl, fake_impl=_rocm_aiter_rmsnorm_fused_dynamic_quant_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_rmsnorm_fused_add_dynamic_quant", op_func=_rocm_aiter_rmsnorm_fused_add_dynamic_quant_impl, fake_impl=_rocm_aiter_rmsnorm_fused_add_dynamic_quant_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_rmsnorm_fp8_group_quant", op_func=_rocm_aiter_rmsnorm_fp8_group_quant_impl, fake_impl=_rocm_aiter_rmsnorm_fp8_group_quant_fake, ) direct_register_custom_op( op_name="rocm_aiter_fused_rms_gated_fp8_group_quant", op_func=_rocm_aiter_fused_rms_gated_fp8_group_quant_impl, fake_impl=_rocm_aiter_fused_rms_gated_fp8_group_quant_fake, ) direct_register_custom_op( op_name="rocm_aiter_rmsnorm_with_add_fp8_group_quant", op_func=_rocm_aiter_rmsnorm_with_add_fp8_group_quant_impl, fake_impl=_rocm_aiter_rmsnorm_with_add_fp8_group_quant_fake, ) direct_register_custom_op( op_name="rocm_aiter_act_mul_and_fp8_group_quant", op_func=_rocm_aiter_act_mul_and_fp8_group_quant_impl, fake_impl=_rocm_aiter_act_mul_and_fp8_group_quant_fake, ) direct_register_custom_op( op_name="rocm_aiter_triton_add_rmsnorm_pad", op_func=_rocm_aiter_triton_add_rmsnorm_pad_impl, fake_impl=_rocm_aiter_triton_add_rmsnorm_pad_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_group_fp8_quant", op_func=_rocm_aiter_group_fp8_quant_impl, fake_impl=_rocm_aiter_group_fp8_quant_fake, ) direct_register_custom_op( op_name="rocm_aiter_per_tensor_quant", op_func=_rocm_aiter_per_tensor_quant_impl, mutates_args=["out", "scale"], dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_per_token_quant", op_func=_rocm_aiter_per_token_quant_impl, fake_impl=_rocm_aiter_per_token_quant_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_sparse_attn_indexer", op_func=rocm_aiter_sparse_attn_indexer, mutates_args=["topk_indices_buffer", "candidate_blocks"], fake_impl=rocm_aiter_sparse_attn_indexer_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="aiter_fp8_attn_wrapper", op_func=_rocm_aiter_fp8_attn_impl, mutates_args=[], fake_impl=_rocm_aiter_fp8_attn_fake, dispatch_key=current_platform.dispatch_key, ) direct_register_custom_op( op_name="rocm_aiter_gemm_a8wfp4", op_func=_rocm_aiter_gemm_a8wfp4_impl, mutates_args=[], fake_impl=_rocm_aiter_gemm_a8wfp4_fake, dispatch_key=current_platform.dispatch_key, ) # Register rocm aiter rotary embedding custom op direct_register_custom_op( op_name="rocm_aiter_triton_rotary_embedding", op_func=_triton_rotary_embedding_impl, mutates_args=["query", "key"], # These tensors are modified in-place ) direct_register_custom_op( op_name="rocm_aiter_fused_allreduce_rmsnorm", op_func=_rocm_aiter_fused_allreduce_rmsnorm_impl, fake_impl=_rocm_aiter_fused_allreduce_rmsnorm_fake, ) direct_register_custom_op( op_name="rocm_aiter_fused_allreduce_rmsnorm_quant_per_group", op_func=(_rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_impl), fake_impl=(_rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_fake), ) direct_register_custom_op( op_name="rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm", # noqa: E501 op_func=_rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_impl, # noqa: E501 fake_impl=_rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_fake, # noqa: E501 ) direct_register_custom_op( op_name="fused_mla_dual_rms_norm", op_func=_fused_mla_dual_rms_norm_impl, mutates_args=[], fake_impl=_fused_mla_dual_rms_norm_fake, ) direct_register_custom_op( op_name="fused_mla_dual_rms_norm_per_token_quant", op_func=_fused_mla_dual_rms_norm_per_token_quant_impl, mutates_args=[], fake_impl=_fused_mla_dual_rms_norm_per_token_quant_fake, ) direct_register_custom_op( op_name="fused_mla_dual_rms_norm_group_quant", op_func=_fused_mla_dual_rms_norm_group_quant_impl, mutates_args=[], fake_impl=_fused_mla_dual_rms_norm_group_quant_fake, ) _OPS_REGISTERED = True @staticmethod def get_rmsnorm_fused_add_dynamic_quant_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_rmsnorm_fused_add_dynamic_quant.default @staticmethod def get_rmsnorm_fused_dynamic_quant_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_rmsnorm_fused_dynamic_quant.default @staticmethod def get_rmsnorm_group_fused_quant_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_rmsnorm_fp8_group_quant.default @staticmethod def get_fused_rms_gated_fp8_group_quant_op() -> OpOverload: """Return the fused gated-RMSNorm + FP8 group quant custom op.""" return torch.ops.vllm.rocm_aiter_fused_rms_gated_fp8_group_quant.default @staticmethod def get_rmsnorm_group_add_fused_quant_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_rmsnorm_with_add_fp8_group_quant.default @staticmethod def get_per_token_quant_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_per_token_quant.default @staticmethod def get_group_quant_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_group_fp8_quant.default @staticmethod def get_act_mul_fused_fp8_group_quant_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_act_mul_and_fp8_group_quant.default @staticmethod def get_triton_add_rmsnorm_pad_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_triton_add_rmsnorm_pad.default @staticmethod def get_triton_rotary_embedding_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_triton_rotary_embedding.default @staticmethod def get_fused_allreduce_rmsnorm_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_fused_allreduce_rmsnorm.default @staticmethod def get_fused_allreduce_rmsnorm_quant_per_group_op() -> OpOverload: return torch.ops.vllm.rocm_aiter_fused_allreduce_rmsnorm_quant_per_group.default @staticmethod def get_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_op() -> OpOverload: # noqa: E501 return torch.ops.vllm.rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm.default # noqa: E501 @staticmethod def get_fused_mla_dual_rms_norm_op() -> OpOverload: return torch.ops.vllm.fused_mla_dual_rms_norm.default @staticmethod def get_fused_mla_dual_rms_norm_per_token_quant_op() -> OpOverload: return torch.ops.vllm.fused_mla_dual_rms_norm_per_token_quant.default @staticmethod def fused_qk_rmsnorm_group_quant( q: torch.Tensor, q_weight: torch.Tensor, q_epsilon: float, kv: torch.Tensor, kv_weight: torch.Tensor, kv_epsilon: float, group_size: int = 128, transpose_scale: bool = False, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Fused q/kv RMSNorm + per-1x128 FP8 group quant on q via AITER.""" return torch.ops.vllm.fused_mla_dual_rms_norm_group_quant( q, q_weight, kv, kv_weight, q_epsilon, kv_epsilon, group_size, transpose_scale, ) @staticmethod def get_fused_mla_dual_rms_norm_group_quant_op() -> OpOverload: return torch.ops.vllm.fused_mla_dual_rms_norm_group_quant.default @staticmethod def rms_norm( x: torch.Tensor, weight: torch.Tensor, epsilon: float, ) -> torch.Tensor: """RMSNorm via AITER kernel.""" import aiter return aiter.rms_norm(x, weight, epsilon) @staticmethod def rms_norm2d_with_add( x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float, ) -> tuple[torch.Tensor, torch.Tensor]: """Fused residual-add + RMSNorm via AITER kernel. Returns (normalized_output, residual_sum). """ import aiter out = torch.empty_like(x) residual_out = torch.empty_like(x) aiter.rmsnorm2d_fwd_with_add(out, x, residual, residual_out, weight, epsilon, 0) return out, residual_out @staticmethod def w8a8_gemm( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_w8a8_gemm(A, B, As, Bs, bias, output_dtype) @staticmethod def preshuffled_per_token_w8a8_gemm( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: return torch.ops.vllm._rocm_aiter_preshuffled_per_token_w8a8_gemm( A, B, As, Bs, bias, output_dtype ) @staticmethod def hipb_mm_fp8( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, bias: torch.Tensor | None = None, output_dtype: torch.dtype = torch.bfloat16, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_hipb_mm_fp8(A, B, As, Bs, bias, output_dtype) @staticmethod def triton_gemm_a8w8_blockscale( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, block_size: list[int], output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_triton_gemm_a8w8_blockscale( A, B, As, Bs, output_dtype ) @staticmethod def gemm_a8w8_blockscale( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, block_size: list[int], output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_gemm_a8w8_blockscale( A, B, As, Bs, output_dtype ) @staticmethod def gemm_a8w8_blockscale_bpreshuffle( A: torch.Tensor, B: torch.Tensor, As: torch.Tensor, Bs: torch.Tensor, output_dtype: torch.dtype = torch.float16, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_gemm_a8w8_blockscale_bpreshuffle( A, B, As, Bs, output_dtype ) @staticmethod def fused_moe( hidden_states: torch.Tensor, w1: torch.Tensor, w2: torch.Tensor, topk_weight: torch.Tensor, topk_ids: torch.Tensor, expert_mask: torch.Tensor | None = None, activation_method: int = 0, quant_method: int = 0, doweight_stage1: bool = False, w1_scale: torch.Tensor | None = None, w2_scale: torch.Tensor | None = None, a1_scale: torch.Tensor | None = None, a2_scale: torch.Tensor | None = None, num_local_tokens: torch.Tensor | None = None, output_dtype: torch.dtype | None = None, hidden_pad: int = 0, intermediate_pad: int = 0, gate_mode: str = "", bias1: torch.Tensor | None = None, bias2: torch.Tensor | None = None, moe_sorting_dispatch_policy: int = 0, swiglu_limit: float = 0.0, beta: float | None = None, linear_beta: float | None = None, shared_w1: torch.Tensor | None = None, shared_w2: torch.Tensor | None = None, shared_w1_scale: torch.Tensor | None = None, shared_w2_scale: torch.Tensor | None = None, shared_expert_id: int = -1, q_dtype_a: torch.dtype | None = None, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_fused_moe( hidden_states, w1, w2, topk_weight, topk_ids, expert_mask, activation_method, quant_method, doweight_stage1, w1_scale, w2_scale, a1_scale, a2_scale, num_local_tokens, output_dtype, hidden_pad, intermediate_pad, gate_mode, bias1, bias2, moe_sorting_dispatch_policy, swiglu_limit, beta, linear_beta, shared_w1, shared_w2, shared_w1_scale, shared_w2_scale, shared_expert_id, q_dtype_a, ) @staticmethod def asm_moe_tkw1( hidden_states: torch.Tensor, w1: torch.Tensor, w2: torch.Tensor, topk_weights: torch.Tensor, topk_ids: torch.Tensor, fc1_scale: torch.Tensor | None = None, fc2_scale: torch.Tensor | None = None, fc1_smooth_scale: torch.Tensor | None = None, fc2_smooth_scale: torch.Tensor | None = None, a16: bool = False, per_tensor_quant_scale: torch.Tensor | None = None, expert_mask: torch.Tensor | None = None, activation_method: int = 0, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_asm_moe_tkw1( hidden_states, w1, w2, topk_weights, topk_ids, fc1_scale, fc2_scale, fc1_smooth_scale, fc2_smooth_scale, a16, per_tensor_quant_scale, expert_mask, activation_method, ) @staticmethod def topk_softmax( topk_weights: torch.Tensor, topk_indices: torch.Tensor, token_expert_indices: torch.Tensor, gating_output: torch.Tensor, renormalize: bool, num_shared_experts: int = 0, shared_expert_scoring_func: str = "", ) -> tuple[torch.Tensor, ...]: torch.ops.vllm.rocm_aiter_topk_softmax( topk_weights, topk_indices, token_expert_indices, gating_output, renormalize, num_shared_experts, shared_expert_scoring_func, ) return topk_weights, topk_indices @staticmethod def topk_sigmoid( topk_weights: torch.Tensor, topk_indices: torch.Tensor, token_expert_indices: torch.Tensor, gating_output: torch.Tensor, renormalize: bool, ) -> tuple[torch.Tensor, ...]: torch.ops.vllm.rocm_aiter_topk_sigmoid( topk_weights, topk_indices, gating_output ) return topk_weights, topk_indices @staticmethod def biased_grouped_topk( gating_output: torch.Tensor, correction_bias: torch.Tensor, topk_weights: torch.Tensor, topk_ids: torch.Tensor, num_expert_group: int, topk_group: int, need_renorm: bool, routed_scaling_factor: float = 1.0, ) -> None: if correction_bias.dtype != gating_output.dtype: correction_bias = correction_bias.to(gating_output.dtype) torch.ops.vllm.rocm_aiter_biased_grouped_topk( gating_output, correction_bias, topk_weights, topk_ids, num_expert_group, topk_group, need_renorm, routed_scaling_factor, ) @staticmethod def grouped_topk( gating_output: torch.Tensor, topk_weights: torch.Tensor, topk_ids: torch.Tensor, num_expert_group: int, topk_group: int, need_renorm: bool, scoring_func: str = "softmax", routed_scaling_factor: float = 1.0, ) -> None: torch.ops.vllm.rocm_aiter_grouped_topk( gating_output, topk_weights, topk_ids, num_expert_group, topk_group, need_renorm, scoring_func, routed_scaling_factor, ) @staticmethod def fused_topk( x: torch.Tensor, router_logits: torch.Tensor, top_k: int, gate_up: bool, ) -> tuple[torch.Tensor, torch.Tensor]: return torch.ops.vllm.rocm_aiter_fused_topk(x, router_logits, top_k, gate_up) @staticmethod def triton_sparse_mla_fwd( q: torch.Tensor, kv_buffer: torch.Tensor, o: torch.Tensor, sm_scale: float, kv_indptr: torch.Tensor, kv_indices: torch.Tensor, kv_lora_rank: int = 512, qk_rope_head_dim: int = 64, q_scale: torch.Tensor | None = None, kv_scale: torch.Tensor | None = None, attn_sink: torch.Tensor | None = None, extra_kv_buffer: torch.Tensor | None = None, extra_kv_indptr: torch.Tensor | None = None, extra_kv_indices: torch.Tensor | None = None, has_invalid: bool = True, ) -> None: """Sparse MLA read straight from the KV cache, for prefill and decode. kv_indices are ragged global slot ids. With has_invalid they may hold -1, which the kernel masks; a caller whose stream never holds a negative slot passes False, which lets the fp8 kernel stage K straight into LDS. The cache format (bf16, per-tensor fp8, or DeepSeek V4's paged fp8_ds_mla) is inferred from kv_buffer and kv_scale. An fp8 q must come with its q_scale and runs both dots in fp8. The extra segment is DeepSeek V4's second cache: SWA window in kv_buffer, top-k compressed tokens here. """ from aiter.ops.triton.attention.sparse_mla import sparse_mla_fwd fp8_q = q.dtype == FP8_DTYPE sparse_mla_fwd( q, kv_buffer, kv_indptr, kv_indices, sm_scale, kv_scale=kv_scale, kv_lora_rank=kv_lora_rank, qk_rope_head_dim=qk_rope_head_dim, has_invalid=has_invalid, dot_precision="fp8" if fp8_q else "bf16", q_scale=q_scale if fp8_q else None, out=o, attn_sink=attn_sink, extra_kv=extra_kv_buffer, extra_indptr=extra_kv_indptr, extra_indices=extra_kv_indices, ) @staticmethod def gather_kv_b_proj( kv_cache: torch.Tensor, kv_scale: torch.Tensor, kv_indptr: torch.Tensor, kv_indices: torch.Tensor, cu_seq_lens: torch.Tensor, weight: torch.Tensor, k: torch.Tensor, v: torch.Tensor, ) -> None: torch.ops.vllm.rocm_aiter_gather_kv_b_proj( kv_cache, kv_scale, kv_indptr, kv_indices, cu_seq_lens, weight, k, v ) @staticmethod def mla_decode_fwd( q: torch.Tensor, kv_buffer: torch.Tensor, o: torch.Tensor, sm_scale: float, qo_indptr: torch.Tensor, max_seqlen_qo: int, kv_indptr: torch.Tensor | None = None, kv_indices: torch.Tensor | None = None, kv_last_page_lens: torch.Tensor | None = None, logit_cap: float = 0.0, q_scale: torch.Tensor | None = None, kv_scale: torch.Tensor | None = None, work_meta_data: torch.Tensor | None = None, work_indptr: torch.Tensor | None = None, work_info_set: torch.Tensor | None = None, reduce_indptr: torch.Tensor | None = None, reduce_final_map: torch.Tensor | None = None, reduce_partial_map: torch.Tensor | None = None, causal: bool = True, ): torch.ops.vllm.rocm_aiter_mla_decode_fwd( q, kv_buffer.view(-1, 1, 1, q.shape[-1]), o, qo_indptr, max_seqlen_qo, kv_indptr, kv_indices, kv_last_page_lens, sm_scale=sm_scale, logit_cap=logit_cap, q_scale=q_scale, kv_scale=kv_scale, work_meta_data=work_meta_data, work_indptr=work_indptr, work_info_set=work_info_set, reduce_indptr=reduce_indptr, reduce_final_map=reduce_final_map, reduce_partial_map=reduce_partial_map, causal=causal, ) @staticmethod def mla_decode_fwd_lse( q: torch.Tensor, kv_buffer: torch.Tensor, o: torch.Tensor, sm_scale: float, qo_indptr: torch.Tensor, max_seqlen_qo: int, kv_indptr: torch.Tensor | None = None, kv_indices: torch.Tensor | None = None, kv_last_page_lens: torch.Tensor | None = None, logit_cap: float = 0.0, q_scale: torch.Tensor | None = None, kv_scale: torch.Tensor | None = None, ) -> torch.Tensor: """Run MLA decode without persistent metadata and return its LSE.""" return torch.ops.vllm.rocm_aiter_mla_decode_fwd_lse( q, kv_buffer.view(-1, 1, 1, q.shape[-1]), o, qo_indptr, max_seqlen_qo, kv_indptr, kv_indices, kv_last_page_lens, sm_scale=sm_scale, logit_cap=logit_cap, q_scale=q_scale, kv_scale=kv_scale, ) @staticmethod def per_tensor_quant( x: torch.Tensor, quant_dtype: torch.dtype, scale: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: out = torch.empty_like(x, dtype=quant_dtype) is_dynamic = scale is None if is_dynamic: scale = torch.empty(1, dtype=torch.float32, device=x.device) torch.ops.vllm.rocm_aiter_per_tensor_quant(out, x, scale, is_dynamic) return out, scale @staticmethod def per_token_quant( x: torch.Tensor, quant_dtype: torch.dtype, scale: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: return torch.ops.vllm.rocm_aiter_per_token_quant(x, quant_dtype, scale) @staticmethod def gemm_a8wfp4( x: torch.Tensor, w: torch.Tensor, x_scales: torch.Tensor, w_scales: torch.Tensor, out_dtype: torch.dtype, ) -> torch.Tensor: return torch.ops.vllm.rocm_aiter_gemm_a8wfp4( x, w, x_scales, w_scales, out_dtype ) @staticmethod def triton_fp4_gemm_dynamic_quant( x: torch.Tensor, weight: torch.Tensor, weight_scale: torch.Tensor, out_dtype: torch.dtype | None = torch.bfloat16, x_scales: torch.Tensor | None = None, ) -> torch.Tensor: from aiter.ops.triton.gemm_afp4wfp4 import gemm_afp4wfp4 from aiter.ops.triton.quant import dynamic_mxfp4_quant if x_scales is None: x_q, x_s = dynamic_mxfp4_quant(x) else: x_q = x x_s = x_scales y = torch.empty( x_q.shape[0], weight.shape[0], device=x_q.device, dtype=out_dtype ) gemm_afp4wfp4(x_q, weight, x_s, weight_scale.T, out_dtype, y) return y @staticmethod def fused_qk_norm_rope_and_cache( qkv: torch.Tensor, q_weight: torch.Tensor, k_weight: torch.Tensor, cos_sin_cache: torch.Tensor, positions: torch.Tensor, num_heads_q: int, num_heads_k: int, num_heads_v: int, head_dim: int, is_neox: bool, rms_norm_eps: float, q_out: torch.Tensor, k_cache: torch.Tensor, v_cache: torch.Tensor, slot_mapping: torch.Tensor, k_scale: torch.Tensor, v_scale: torch.Tensor, k_out: torch.Tensor | None, v_out: torch.Tensor | None, return_kv: bool, use_shuffle_layout: bool, block_size: int, x: int, rotary_dim: int = 0, ): from aiter.ops.fused_qk_norm_rope_cache_quant import ( fused_qk_norm_rope_cache_pts_quant_shuffle, ) fused_qk_norm_rope_cache_pts_quant_shuffle( qkv, q_weight, k_weight, cos_sin_cache, positions, qkv.size(0), num_heads_q, num_heads_k, num_heads_v, head_dim, is_neox, rms_norm_eps, q_out, k_cache, v_cache, slot_mapping, k_scale, v_scale, k_out, v_out, return_kv, use_shuffle_layout, block_size, x, rotary_dim, ) @staticmethod def do_qk_norm_rope_kvcache_update( qkv: torch.Tensor, q_weight: torch.Tensor, k_weight: torch.Tensor, cos_sin_cache: torch.Tensor, positions: torch.Tensor, num_heads_q: int, num_heads_k: int, head_dim: int, is_neox: bool, rms_norm_eps: float, q_out: torch.Tensor, k_out: torch.Tensor, key_cache: torch.Tensor, value_cache: torch.Tensor, slot_mapping: torch.Tensor, k_scale: torch.Tensor, v_scale: torch.Tensor, kv_cache_dtype: str, use_shuffle_layout: bool, ) -> None: """Run the fused QK-norm+RoPE+KV-cache op on already-split k/v caches. Shared by the AITER FA and unified-attention impls. The caller splits kv_cache, since the unbind dim depends on the layout (e.g. the unified encoder-decoder path is K/V-first), and passes use_shuffle_layout (unified reads NHD and must pass False). """ if kv_cache_dtype.startswith("fp8"): key_cache = key_cache.view(current_platform.fp8_dtype()) value_cache = value_cache.view(current_platform.fp8_dtype()) # Partial-rotary support (e.g. GLM-4.7 applies rotary to only a prefix # of each head's channel dim). rotary_dim = cos_sin_cache.shape[-1] kernel_rotary_dim = 0 if rotary_dim == head_dim else rotary_dim rocm_aiter_ops.fused_qk_norm_rope_and_cache( qkv=qkv, q_weight=q_weight, k_weight=k_weight, cos_sin_cache=cos_sin_cache, positions=positions, num_heads_q=num_heads_q, num_heads_k=num_heads_k, num_heads_v=num_heads_k, head_dim=head_dim, is_neox=is_neox, rms_norm_eps=rms_norm_eps, q_out=q_out, k_cache=key_cache, v_cache=value_cache, slot_mapping=slot_mapping, k_scale=k_scale, v_scale=v_scale, k_out=k_out, v_out=None, return_kv=True, use_shuffle_layout=use_shuffle_layout, block_size=key_cache.shape[1], x=16 // key_cache.element_size(), rotary_dim=kernel_rotary_dim, ) @staticmethod def fused_qk_norm_mrope_and_cache( qkv: torch.Tensor, q_weight: torch.Tensor, k_weight: torch.Tensor, cos_sin_cache: torch.Tensor, positions: torch.Tensor, num_heads_q: int, num_heads_k: int, head_dim: int, is_neox: bool, mrope_section: list[int], is_interleaved: bool, rms_norm_eps: float, q_out: torch.Tensor, k_cache: torch.Tensor, v_cache: torch.Tensor, slot_mapping: torch.Tensor, k_scale: torch.Tensor, v_scale: torch.Tensor, block_size: int, x: int, rotary_dim: int = 0, ) -> None: from aiter.ops.fused_qk_norm_mrope_cache_quant import ( fused_qk_norm_mrope_3d_cache_pts_quant_shuffle, ) fused_qk_norm_mrope_3d_cache_pts_quant_shuffle( qkv, q_weight, k_weight, cos_sin_cache, positions, qkv.size(0), num_heads_q, num_heads_k, num_heads_k, head_dim, is_neox, mrope_section, is_interleaved, rms_norm_eps, q_out, k_cache, v_cache, slot_mapping, k_scale, v_scale, None, None, False, False, block_size, x, rotary_dim, ) @staticmethod def do_qk_norm_mrope_kvcache_update( qkv: torch.Tensor, q_weight: torch.Tensor, k_weight: torch.Tensor, cos_sin_cache: torch.Tensor, positions: torch.Tensor, num_heads_q: int, num_heads_k: int, head_dim: int, is_neox: bool, mrope_section: list[int], is_interleaved: bool, rms_norm_eps: float, q_out: torch.Tensor, key_cache: torch.Tensor, value_cache: torch.Tensor, slot_mapping: torch.Tensor, k_scale: torch.Tensor, v_scale: torch.Tensor, kv_cache_dtype: str, rotary_dim: int = 0, ) -> None: if kv_cache_dtype.startswith("fp8"): key_cache = key_cache.view(current_platform.fp8_dtype()) value_cache = value_cache.view(current_platform.fp8_dtype()) rocm_aiter_ops.fused_qk_norm_mrope_and_cache( qkv=qkv, q_weight=q_weight, k_weight=k_weight, cos_sin_cache=cos_sin_cache, positions=positions, num_heads_q=num_heads_q, num_heads_k=num_heads_k, head_dim=head_dim, is_neox=is_neox, mrope_section=mrope_section, is_interleaved=is_interleaved, rms_norm_eps=rms_norm_eps, q_out=q_out, k_cache=key_cache, v_cache=value_cache, slot_mapping=slot_mapping, k_scale=k_scale, v_scale=v_scale, block_size=key_cache.shape[1], x=16 // key_cache.element_size(), rotary_dim=rotary_dim, ) @staticmethod def triton_rope_and_cache( query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, positions: torch.Tensor, cos_sin_cache: torch.Tensor, is_neox: bool, key_cache: torch.Tensor, value_cache: torch.Tensor, layer_slot_mapping: torch.Tensor, k_scale: torch.Tensor, v_scale: torch.Tensor, flash_layout: bool, apply_scale: bool, ): from aiter.ops.triton.fused_kv_cache import fused_qk_rope_reshape_and_cache cos, sin = cos_sin_cache.chunk(2, dim=-1) fused_qk_rope_reshape_and_cache( query, key, value, key_cache, value_cache, layer_slot_mapping, positions, cos, sin, k_scale, v_scale, is_neox, flash_layout=flash_layout, apply_scale=apply_scale, q_out=query, k_out=key, output_zeros=False, ) @staticmethod def batched_gemm_a16wfp4( X: torch.Tensor, W: torch.Tensor, w_scale: torch.Tensor, Y: torch.Tensor | None = None, transpose_bm: bool | None = False, prequant: bool | None = False, y_scale: torch.Tensor | None = None, ) -> torch.Tensor: # ruff: noqa: E501 # isort: skip from aiter.ops.triton.batched_gemm_a16wfp4 import batched_gemm_a16wfp4 return batched_gemm_a16wfp4( X, W, w_scale, y=Y, transpose_bm=transpose_bm, prequant=prequant, y_scale=y_scale, ) @staticmethod def triton_fp8_bmm( X: torch.Tensor, WQ: torch.Tensor, w_scale: torch.Tensor, group_size: int = 128, bias: torch.Tensor | None = None, dtype: torch.dtype | None = torch.bfloat16, splitK: int | None = None, YQ: torch.Tensor | None = None, transpose_bm: bool | None = False, config: dict | None = None, ) -> torch.Tensor: # ruff: noqa: E501 # isort: skip from aiter.ops.triton.batched_gemm_a8w8_a_per_token_group_prequant_w_per_batched_tensor_quant import ( batched_gemm_a8w8_a_per_token_group_prequant_w_per_batched_tensor_quant as aiter_triton_fp8_bmm, ) return aiter_triton_fp8_bmm( X, WQ, w_scale, group_size=group_size, bias=bias, dtype=dtype, splitK=splitK, YQ=YQ, transpose_bm=transpose_bm, config=config, ) @staticmethod def group_fp8_quant( input_2d: torch.Tensor, group_size: int = 128, transpose_scale: bool = False, ) -> tuple[torch.Tensor, torch.Tensor]: assert group_size == 128, "Group size must be 128" return torch.ops.vllm.rocm_aiter_group_fp8_quant( input_2d, group_size, transpose_scale ) @staticmethod @functools.cache def is_triton_gemm_w8a8_tuned(n: int, k: int) -> bool: if not current_platform.is_rocm(): return False try: return _triton_gemm_config_is_tuned("GEMM-A8W8_BLOCKSCALE", n, k) except (AssertionError, ImportError): return False @staticmethod @functools.cache def is_triton_gemm_afp4wfp4_presh_ws_tuned(n: int, k_bytes: int) -> bool: if not current_platform.is_rocm(): return False # Input = weight.shape[1] (bytes); *4 for fp4 and aiter config 2*k try: return _triton_gemm_config_is_tuned( "GEMM-AFP4WFP4_PRESHUFFLED", n, 4 * k_bytes ) except (AssertionError, ImportError): return False @staticmethod @functools.cache def is_shuffled_per_token_w8a8_gemm_tuned( N: int, K: int, q_dtype_w: torch.dtype ) -> bool: return _ck_gemm_shape_is_tuned( N, K, q_dtype_w, "AITER_CONFIG_GEMM_A8W8_BPRESHUFFLE_FILE" ) @staticmethod @functools.cache def is_per_token_w8a8_gemm_tuned(N: int, K: int, q_dtype_w: torch.dtype) -> bool: return _ck_gemm_shape_is_tuned(N, K, q_dtype_w, "AITER_CONFIG_GEMM_A8W8_FILE") @staticmethod def shuffle_weight( tensor: torch.Tensor, layout: tuple[int, int] = (16, 16) ) -> torch.Tensor: from aiter.ops.shuffle import shuffle_weight return shuffle_weight(tensor, layout=layout) @staticmethod def shuffle_weight_a16w4( tensor: "torch.Tensor", nLane: int, gate_up: bool, ) -> "torch.Tensor": """Shuffles the weight tensor into (A16W4) layout for AITER kernels. Args: tensor: The input weight tensor to be shuffled. nLane: Number of lanes in the shuffle layout. gate_up: Whether the weight is for w13 (True) or w2 (False). Returns: torch.Tensor: The shuffled tensor. """ from aiter.ops.shuffle import shuffle_weight_a16w4 return shuffle_weight_a16w4(tensor, nLane, gate_up) @staticmethod def shuffle_scale_a16w4( tensor: "torch.Tensor", num_experts: int, gate_up: bool, ) -> "torch.Tensor": """Shuffles the scale tensor into (A16W4) layout for AITER kernels. Args: tensor: The input scale tensor to be shuffled. num_experts: Number of experts, needed for reshaping logic. gate_up: Whether the scale is for w13 (True) or w2 (False). Returns: torch.Tensor: The shuffled scale tensor. """ from aiter.ops.shuffle import shuffle_scale_a16w4 return shuffle_scale_a16w4(tensor, num_experts, gate_up) @staticmethod def shuffle_scale(tensor: torch.Tensor) -> torch.Tensor: """Shuffle a non-gate/up E8M0 scale tensor for AITER.""" from aiter.ops.shuffle import shuffle_scale return shuffle_scale(tensor) @staticmethod def shuffle_weights( *tensors: torch.Tensor, layout: tuple[int, int] = (16, 16) ) -> tuple[torch.Tensor, ...]: """Applies shuffle_weight function from AITER to each input tensor and returns them. Rearranges (shuffles) the input tensor/s into a specified block layout for optimized computation. Args: *tensors: Variable number of torch.Tensor objects. layout: A pair of integers specifying the block sizes used to divide the tensors during shuffling. Default is (16, 16). Returns: A Tuple of shuffled tensors. """ from aiter.ops.shuffle import shuffle_weight return tuple(shuffle_weight(tensor, layout=layout) for tensor in tensors) @staticmethod def shuffle_mxfp8_moe_weights( w13: torch.Tensor, w2: torch.Tensor, w13_scale: torch.Tensor, w2_scale: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Preshuffle MXFP8 MoE weights + E8M0 scales into AITER's FlyDSL layout: gate/up-interleaved weights, interleaved scale for w13 (gate/up), plain scale for w2 (the interleaved variant is gate/up-only and misaligns w2). """ from aiter.ops.shuffle import shuffle_scale, shuffle_weight num_experts = w13.shape[0] w13 = shuffle_weight(w13, is_guinterleave=True, gate_up=True) w2 = shuffle_weight(w2, is_guinterleave=True, gate_up=False) w13_scale = shuffle_scale( w13_scale.reshape(-1, w13_scale.shape[-1]), num_experts, is_guinterleave=True, gate_up=True, ) w2_scale = shuffle_scale(w2_scale.reshape(-1, w2_scale.shape[-1])) return w13, w2, w13_scale, w2_scale @staticmethod def flash_attn_varlen_func( q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, cu_seqlens_q: torch.Tensor, cu_seqlens_k: torch.Tensor, max_seqlen_q: int, max_seqlen_k: int, min_seqlen_q: int | None = None, dropout_p: float = 0.0, softmax_scale: float | None = None, causal: bool = False, window_size: tuple[int, int] | None = None, alibi_slopes: torch.Tensor | None = None, return_lse: bool = False, out: torch.Tensor | None = None, sink_ptr: torch.Tensor | None = None, ): """Flash attention with variable length sequences. This function is NOT wrapped with @is_aiter_supported decorator to allow explicit backend selection via attention_config to work even when VLLM_ROCM_USE_AITER=0. Note: This performs lazy import of aiter.flash_attn_varlen_func """ from aiter import flash_attn_varlen_func return flash_attn_varlen_func( q=q, k=k, v=v, cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_k, max_seqlen_q=max_seqlen_q, max_seqlen_k=max_seqlen_k, min_seqlen_q=min_seqlen_q, dropout_p=dropout_p, softmax_scale=softmax_scale, causal=causal, window_size=window_size, alibi_slopes=alibi_slopes, return_lse=return_lse, out=out, sink_ptr=sink_ptr, ) @staticmethod def fp8_attn_wrapper( q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_descale: torch.Tensor, k_descale: torch.Tensor, v_descale: torch.Tensor, batch_size: int, output_dtype: torch.dtype, scale: float | None = None, cu_seqlens: torch.Tensor | None = None, max_seqlen: torch.Tensor | None = None, ) -> torch.Tensor: return torch.ops.vllm.aiter_fp8_attn_wrapper( q, k, v, q_descale, k_descale, v_descale, batch_size, output_dtype, scale, cu_seqlens, max_seqlen, ) @staticmethod def pa_fwd_asm( Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor, block_tables: torch.Tensor, context_lens: torch.Tensor, block_tables_stride0: int, K_QScale: torch.Tensor, V_QScale: torch.Tensor, out_: torch.Tensor, ): """Paged attention forward pass using assembly kernel. This function is NOT wrapped with @is_aiter_supported decorator to allow explicit backend selection via attention_config to work even when VLLM_ROCM_USE_AITER=0. Note: This performs lazy import of aiter.pa_fwd_asm """ from aiter import pa_fwd_asm return pa_fwd_asm( Q=Q, K=K, V=V, block_tables=block_tables, context_lens=context_lens, block_tables_stride0=block_tables_stride0, K_QScale=K_QScale, V_QScale=V_QScale, out_=out_, ) @staticmethod def paged_attention_common( Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor, tmp_out: torch.Tensor, max_logits: torch.Tensor, exp_sums: torch.Tensor, max_seq_len: int, block_tables: torch.Tensor, context_lens: torch.Tensor, block_tables_stride0: int, scale: float, K_QScale_hip: torch.Tensor, V_QScale_hip: torch.Tensor, K_QScale_asm: torch.Tensor, V_QScale_asm: torch.Tensor, out_: torch.Tensor, kv_cache_dtype: str, ): """Paged attention common function. This function is NOT wrapped with @is_aiter_supported decorator to allow explicit backend selection via attention_config to work even when VLLM_ROCM_USE_AITER=0. Note: This performs lazy import of aiter.paged_attention_common """ from aiter import paged_attention_common return paged_attention_common( Q=Q, K=K, V=V, tmp_out=tmp_out, max_logits=max_logits, exp_sums=exp_sums, max_seq_len=max_seq_len, block_tables=block_tables, context_lens=context_lens, block_tables_stride0=block_tables_stride0, scale=scale, K_QScale_hip=K_QScale_hip, V_QScale_hip=V_QScale_hip, K_QScale_asm=K_QScale_asm, V_QScale_asm=V_QScale_asm, out_=out_, kv_cache_dtype=kv_cache_dtype, ) @staticmethod def mhc_pre( residual: torch.Tensor, fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, rms_eps: float, hc_pre_eps: float, hc_sinkhorn_eps: float, hc_post_mult_value: float, sinkhorn_repeat: int, norm_weight: torch.Tensor | None = None, norm_eps: float = 0.0, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Forward pass for mHC pre block. Args: residual: shape (..., hc_mult, hidden_size), dtype torch.bfloat16 fn: shape (hc_mult3, hc_mult * hidden_size), dtype torch.float32 hc_scale: shape (3,), dtype torch.float32 hc_base: shape (hc_mult3,), dtype torch.float32 rms_eps: RMS normalization epsilon hc_pre_eps: pre-mix epsilon hc_sinkhorn_eps: sinkhorn epsilon hc_post_mult_value: post-mix multiplier value sinkhorn_repeat: number of sinkhorn iterations norm_weight: optional RMSNorm weight fused into the pre kernel norm_eps: epsilon for the fused RMSNorm when norm_weight is set Returns: post_mix: shape (..., hc_mult), dtype torch.float32 comb_mix: shape (..., hc_mult, hc_mult), dtype torch.float32 layer_input: shape (..., hidden_size), dtype torch.bfloat16 """ from aiter.ops.mhc import mhc_pre # Validate shapes assert residual.dtype == torch.bfloat16 assert fn.dtype == torch.float32 assert hc_scale.dtype == torch.float32 assert hc_base.dtype == torch.float32 hc_mult = residual.shape[-2] hidden_size = residual.shape[-1] hc_mult2 = hc_mult * hc_mult hc_mult3 = hc_mult * 2 + hc_mult2 hc_hidden_size = hc_mult * hidden_size assert fn.shape[0] == hc_mult3 assert fn.shape[1] == hc_hidden_size assert hc_scale.shape == (3,) assert hc_base.shape == (hc_mult3,) outer_shape = residual.shape[:-2] residual_flat = residual.view(-1, hc_mult, hidden_size) num_tokens = residual_flat.shape[0] if num_tokens == 0: return ( torch.empty( num_tokens, hc_mult, 1, dtype=torch.float32, device=residual_flat.device, ), torch.empty( num_tokens, hc_mult, hc_mult, dtype=torch.float32, device=residual_flat.device, ), torch.empty( num_tokens, hidden_size, dtype=torch.bfloat16, device=residual_flat.device, ), ) # AITER's Python wrapper allocates intermediate/output tensors without # explicit device arguments, so run it under the residual tensor's device. with torch.device(residual_flat.device): args = ( residual_flat, fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_mult_value, sinkhorn_repeat, ) if norm_weight is None: post_mix, comb_mix, layer_input = mhc_pre(*args) else: post_mix, comb_mix, layer_input = mhc_pre(*args, norm_weight, norm_eps) return ( post_mix.view(*outer_shape, hc_mult, 1), comb_mix.view(*outer_shape, hc_mult, hc_mult), layer_input.view(*outer_shape, hidden_size), ) @staticmethod def mhc_pre_delayed( residual: torch.Tensor, fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, rms_eps: float, hc_pre_eps: float, hc_sinkhorn_eps: float, hc_post_mult_value: float, sinkhorn_repeat: int, pre_mix: torch.Tensor | None = None, sublayer_out: torch.Tensor | None = None, post_layer_mix: torch.Tensor | None = None, comb_res_mix: torch.Tensor | None = None, residual_out: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """MHC pre using the pre-mix carried from the previous sublayer. Same gates as :meth:`mhc_pre`, but the stream collapse applies the caller's ``pre_mix`` instead of the one computed here, and the one computed here is returned for the next sublayer seam. AITER has no single kernel for that shape, so drive its two stages directly: ``mhc_pre_big_fuse`` still produces the post and comb gates (including every sinkhorn iteration), while the pre gate is recovered from the same split-k GEMM output and the collapse is done by the Triton kernel. ``mhc_pre_big_fuse`` also writes a collapse we do not use; that redundant store is the price of not having a native delayed kernel, and is small next to the ~140 launches it replaces. Passing ``sublayer_out`` / ``post_layer_mix`` / ``comb_res_mix`` also applies the preceding post block. AITER then projects the new residual in the same kernel that computes it (``mhc_fused_post_pre_gemm_sqrsum``), which saves one launch; every later stage is identical either way. That new residual is written to ``residual_out``, which the caller must supply in that case; it is an output buffer rather than a return value so the op never has to hand back one of its own inputs on the unfused path. Returns: post_mix: shape (..., hc_mult, 1), dtype torch.float32 comb_mix: shape (..., hc_mult, hc_mult), dtype torch.float32 layer_input: shape (..., hidden_size), dtype torch.bfloat16 next_pre_mix: shape (..., hc_mult), dtype torch.float32 """ from aiter.ops.mhc import ( get_mhc_fused_post_pre_config, get_mhc_pre_splitk, mhc_fused_post_pre_gemm_sqrsum, mhc_pre_gemm_sqrsum, ) assert residual.dtype == torch.bfloat16 assert fn.dtype == torch.float32 assert hc_scale.dtype == torch.float32 assert hc_base.dtype == torch.float32 hc_mult = residual.shape[-2] hidden_size = residual.shape[-1] hc_mult3 = hc_mult * 2 + hc_mult * hc_mult hc_hidden_size = hc_mult * hidden_size assert fn.shape == (hc_mult3, hc_hidden_size) assert hc_scale.shape == (3,) assert hc_base.shape == (hc_mult3,) outer_shape = residual.shape[:-2] residual_flat = residual.view(-1, hc_mult, hidden_size) num_tokens = residual_flat.shape[0] device = residual_flat.device if num_tokens == 0: return ( torch.empty(0, hc_mult, 1, dtype=torch.float32, device=device), torch.empty(0, hc_mult, hc_mult, dtype=torch.float32, device=device), torch.empty(0, hidden_size, dtype=torch.bfloat16, device=device), torch.empty(0, hc_mult, dtype=torch.float32, device=device), ) pre_mix_flat = None if pre_mix is None else pre_mix.view(-1, hc_mult) fuse_post = sublayer_out is not None # AITER's Python wrappers allocate without explicit device arguments. with torch.device(device): if fuse_post: # Keyed on the per-stream K, i.e. hidden_size, not hc_mult * it. splitk, tile_m, tile_n, tile_k = get_mhc_fused_post_pre_config( num_tokens, hidden_size ) else: splitk, tile_k = get_mhc_pre_splitk(num_tokens, hc_hidden_size) # AITER pads the GEMM output to a multiple of 32 columns. gemm_pad = torch.empty( splitk, num_tokens, (hc_mult3 + 31) // 32 * 32, dtype=torch.float32, device=device, ) gemm_out = gemm_pad[:, :, :hc_mult3] sqrsum = torch.empty(splitk, num_tokens, dtype=torch.float32, device=device) if fuse_post: # Restated for the type checker, which cannot narrow the # optional arguments through the fuse_post flag. assert sublayer_out is not None assert post_layer_mix is not None and comb_res_mix is not None assert residual_out is not None projected = residual_out.view(-1, hc_mult, hidden_size) mhc_fused_post_pre_gemm_sqrsum( gemm_out, sqrsum, projected, sublayer_out.view(-1, hidden_size), residual_flat, # The kernel wants the post mix squeezed to (tokens, hc_mult). post_layer_mix.view(-1, hc_mult, 1).squeeze(-1), comb_res_mix.view(-1, hc_mult, hc_mult), fn, tile_m, tile_n, tile_k, 0, ) else: projected = residual_flat mhc_pre_gemm_sqrsum(gemm_out, sqrsum, projected, fn, tile_k, 0) post_mix, comb_mix, layer_input, next_pre_mix = _mhc_delayed_pre_tail( gemm_out, sqrsum, projected, hc_scale, hc_base, pre_mix_flat, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_mult_value, sinkhorn_repeat, ) return ( post_mix.view(*outer_shape, hc_mult, 1), comb_mix.view(*outer_shape, hc_mult, hc_mult), layer_input.view(*outer_shape, hidden_size), next_pre_mix.view(*outer_shape, hc_mult), ) @staticmethod def mhc_fused_post_pre_delayed_rms_norm( residual: torch.Tensor, fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, rms_eps: float, hc_pre_eps: float, hc_sinkhorn_eps: float, hc_post_mult_value: float, sinkhorn_repeat: int, pre_mix: torch.Tensor | None, sublayer_out: torch.Tensor | None, post_layer_mix: torch.Tensor | None, comb_res_mix: torch.Tensor | None, norm_weight: torch.Tensor, norm_eps: float, residual_out: torch.Tensor | None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """:meth:`mhc_pre_delayed` with the RMSNorm of the collapse folded in. Runs aiter's fused Triton kernel ``mhc_fused_post_pre_delayed_rmsnorm``; the returned ``layer_input`` is already normalised with ``norm_weight`` / ``norm_eps``. """ from aiter.ops.triton.fusions.mhc_fused_post_pre_delayed_rmsnorm import ( mhc_fused_post_pre_delayed_rmsnorm, ) _, post_mix, comb_mix, layer_input, next_pre_mix = ( mhc_fused_post_pre_delayed_rmsnorm( residual, fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_mult_value, sinkhorn_repeat, pre_mix, sublayer_out, post_layer_mix, comb_res_mix, norm_weight, norm_eps, residual_out=residual_out, ) ) return post_mix, comb_mix, layer_input, next_pre_mix @staticmethod def mhc_fused_post_pre_delayed_prefers_unfused(num_tokens: int) -> bool: """True when AITER's own heuristic favours separate post and pre. Mirrors the ``fused_m_upper_bound`` table in ``aiter.ops.mhc``: the fused GEMM wins for decode-sized batches and loses for large ones. """ from aiter.jit.utils.chip_info import get_gfx_runtime return num_tokens >= { "gfx950": 1024, "gfx942": 128, "gfx1250": 1024, }.get(get_gfx_runtime(), 1024) @staticmethod def hc_head( hs_flat: torch.Tensor, fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, out: torch.Tensor, hidden_size: int, rms_eps: float, hc_eps: float, hc_mult: int, ) -> None: """Run hc_head through AITER mhc_pre and write the result to out.""" assert hs_flat.dtype == torch.bfloat16 assert fn.dtype == torch.float32 assert hc_scale.dtype == torch.float32 assert hc_base.dtype == torch.float32 assert hs_flat.shape[-2:] == (hc_mult, hidden_size) assert fn.shape == (hc_mult, hc_mult * hidden_size) assert hc_scale.shape == (1,) assert hc_base.shape == (hc_mult,) num_tokens = hs_flat.shape[0] if num_tokens == 0: return hc_mult3 = hc_mult * 2 + hc_mult * hc_mult full_fn = torch.zeros( hc_mult3, hc_mult * hidden_size, dtype=fn.dtype, device=fn.device, ) full_fn[:hc_mult] = fn full_base = torch.zeros(hc_mult3, dtype=hc_base.dtype, device=hc_base.device) full_base[:hc_mult] = hc_base full_scale = torch.zeros(3, dtype=hc_scale.dtype, device=hc_scale.device) full_scale[0] = hc_scale[0] _, _, layer_input = rocm_aiter_ops.mhc_pre( hs_flat, full_fn, full_scale, full_base, rms_eps, hc_eps, 0.0, 1.0, 0, ) out.copy_(layer_input) @staticmethod def mhc_post( x: torch.Tensor, residual: torch.Tensor, post_layer_mix: torch.Tensor, comb_res_mix: torch.Tensor, ) -> torch.Tensor: from aiter.ops.mhc import mhc_post hc_mult = residual.shape[-2] hidden_size = residual.shape[-1] residual_flat = residual.view(-1, hc_mult, hidden_size) num_tokens = residual_flat.shape[0] out = torch.empty_like(residual_flat) mhc_post( out, x.view(num_tokens, hidden_size), residual_flat, post_layer_mix.view(num_tokens, hc_mult, 1), comb_res_mix.view(num_tokens, hc_mult, hc_mult), ) return out.view_as(residual) @staticmethod def mhc_fused_post_pre( x: torch.Tensor, residual: torch.Tensor, post_layer_mix: torch.Tensor, comb_res_mix: torch.Tensor, fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, rms_eps: float, hc_pre_eps: float, hc_sinkhorn_eps: float, hc_post_mult_value: float, sinkhorn_repeat: int, norm_weight: torch.Tensor | None = None, norm_eps: float = 0.0, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Fused mHC post + next mHC pre via AITER. Returns residual_cur, post_mix, comb_mix, layer_input in vLLM order (AITER returns post_mix, comb_mix, layer_input, next_residual). """ from aiter.ops.mhc import mhc_fused_post_pre assert x.dtype == torch.bfloat16 assert residual.dtype == torch.bfloat16 assert fn.dtype == torch.float32 assert hc_scale.dtype == torch.float32 assert hc_base.dtype == torch.float32 hc_mult = residual.shape[-2] hidden_size = residual.shape[-1] outer_shape = residual.shape[:-2] residual_flat = residual.view(-1, hc_mult, hidden_size) num_tokens = residual_flat.shape[0] x_flat = x.view(num_tokens, hidden_size) post_flat = post_layer_mix.view(num_tokens, hc_mult, 1) comb_flat = comb_res_mix.view(num_tokens, hc_mult, hc_mult) if num_tokens == 0: return ( torch.empty_like(residual_flat).view_as(residual), torch.empty( *outer_shape, hc_mult, 1, dtype=torch.float32, device=residual.device, ), torch.empty( *outer_shape, hc_mult, hc_mult, dtype=torch.float32, device=residual.device, ), torch.empty( *outer_shape, hidden_size, dtype=torch.bfloat16, device=residual.device, ), ) args = ( x_flat, residual_flat, post_flat, comb_flat, fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_mult_value, sinkhorn_repeat, ) with torch.device(residual_flat.device): if norm_weight is None: result = mhc_fused_post_pre(*args) else: result = mhc_fused_post_pre(*args, norm_weight, norm_eps) post_mix, comb_mix, layer_input, next_residual = result return ( next_residual.view_as(residual), post_mix.view(*outer_shape, hc_mult, 1), comb_mix.view(*outer_shape, hc_mult, hc_mult), layer_input.view(*outer_shape, hidden_size), ) rocm_aiter_ops.register_ops_once() _sync_aiter_situv2_moe_env()