[Attention Backend] add HPC-Ops Attention backend (#46020)

Signed-off-by: chengvjiang <chengvjiang@tencent.com> Co-authored-by: chengvjiang <chengvjiang@tencent.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com>

Cheng Jiang committed Jun 30, 2026 at 18:17 UTC 8cf7c4d8ad602d73ff2ec72a101420d47163c136
10 files changed +978 -14
docs/design/attention_backends.md
+1
@@ -167,6 +167,7 @@ Priority is **1 = highest** (tried first).
167 | `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
168 | `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
169 | `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder Only | Any |
170 +| `HPC_ATTN` | | fp16, bf16 | `auto`, `fp8_e4m3` | 64 | 128 | ❌ | ❌ | ❌ | ❌ | Decoder | ≥9.0 |
171 | `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ✅ | ✅ | ❌ | ❌ | Decoder | N/A |
172 | `ROCM_AITER_UNIFIED_ATTN` | | bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | N/A |
173 | `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
vllm/config/compilation.py
+1
@@ -759,6 +759,7 @@ class CompilationConfig:
759 "vllm::sparse_attn_indexer",
760 "vllm::rocm_aiter_sparse_attn_indexer",
761 "vllm::deepseek_v4_attention",
762 + "vllm::hpc_rope_norm_forward",
763 ]
764
765 def compute_hash(self) -> str:
vllm/model_executor/layers/attention/attention.py
+3 -1
@@ -458,6 +458,7 @@ class Attention(nn.Module, AttentionLayerBase):
458 # shape does not match the query shape, so we optionally let the model
459 # definition specify the output tensor shape.
460 output_shape: torch.Size | None = None,
461 + output_dtype: torch.dtype | None = None,
462 ) -> torch.Tensor:
463 """
464 The KV cache is stored inside this class and is accessed via
@@ -472,7 +473,8 @@ class Attention(nn.Module, AttentionLayerBase):
473 torch.ops.vllm.maybe_calc_kv_scales(
474 query, key, value, _encode_layer_name(self.layer_name)
475 )
475 - output_dtype = query.dtype
476 + if output_dtype is None:
477 + output_dtype = query.dtype
478 if self.query_quant is not None:
479 # quantizing with a simple torch operation enables
480 # torch.compile to fuse this into previous ops
vllm/model_executor/layers/hpc/__init__.py new
+11
@@ -0,0 +1,11 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from vllm.model_executor.layers.hpc.hpc_module import HpcModule
5 +from vllm.model_executor.layers.hpc.rope_norm import HpcRopeNorm, QkNormPolicy
6 +
7 +__all__ = [
8 + "HpcModule",
9 + "HpcRopeNorm",
10 + "QkNormPolicy",
11 +]
vllm/model_executor/layers/hpc/hpc_module.py new
+18
@@ -0,0 +1,18 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import torch.nn as nn
4 +
5 +
6 +class HpcModule(nn.Module):
7 + def __init__(self):
8 + super().__init__()
9 +
10 + @classmethod
11 + def support(cls, *args, **kwargs):
12 + return True
13 +
14 + def process_weights_after_loading(self, model):
15 + pass
16 +
17 + def forward(self, *args, **kwargs):
18 + pass
vllm/model_executor/layers/hpc/rope_norm.py new
+408
@@ -0,0 +1,408 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""HPC fused RoPE + QK-Norm + KV-Cache-Write (+ optional FP8 Q quant).
4 +
5 +Decoupled from HpcAttentionImpl; extra params are passed via layer attrs.
6 +"""
7 +
8 +from __future__ import annotations
9 +
10 +from enum import IntEnum
11 +from typing import Any
12 +
13 +import torch
14 +
15 +from vllm.config import get_current_vllm_config_or_none
16 +from vllm.forward_context import ForwardContext, get_forward_context
17 +from vllm.logger import init_logger
18 +from vllm.model_executor.custom_op import CustomOp
19 +from vllm.model_executor.layers.hpc.hpc_module import HpcModule
20 +from vllm.utils.torch_utils import direct_register_custom_op
21 +from vllm.v1.attention.backends.hpc_attn import HpcAttnMetadata
22 +from vllm.v1.attention.backends.registry import AttentionBackendEnum
23 +
24 +logger = init_logger(__name__)
25 +
26 +_hpc_rope_norm_instances: dict[str, HpcRopeNorm] = {}
27 +
28 +
29 +class QkNormPolicy(IntEnum):
30 + """Order of QK-RMSNorm relative to RoPE in the fused HPC rope_norm kernel.
31 +
32 + The values are part of the HPC kernel ABI (passed through as ints), so they
33 + must stay in sync with the kernel's expectations.
34 + """
35 +
36 + # No QK-Norm: apply RoPE only.
37 + NONE = 0
38 + # Apply RoPE first, then QK-RMSNorm.
39 + ROPE_THEN_NORM = 1
40 + # Apply QK-RMSNorm first, then RoPE (e.g. HunYuan V3).
41 + NORM_THEN_ROPE = 2
42 +
43 +
44 +def hpc_rope_norm_forward(
45 + qkv: torch.Tensor,
46 + output: torch.Tensor,
47 + layer_name: str,
48 +) -> None:
49 + """Top-level custom op: RoPE + QK-Norm + KV-Cache-Write + FP8 Q quant.
50 +
51 + Fully opaque to torch.compile (dynamo).
52 + """
53 + forward_context: ForwardContext = get_forward_context()
54 + attn_metadata: Any = forward_context.attn_metadata
55 + if isinstance(attn_metadata, dict):
56 + attn_metadata = attn_metadata[layer_name]
57 +
58 + if attn_metadata is None:
59 + output.zero_()
60 + return
61 +
62 + attn_layer = forward_context.no_compile_layers[layer_name]
63 + # bind_kv_cache stores the per-layer KV cache as a single 5D tensor
64 + # (num_blocks, 2, block_size, num_kv_heads, head_size), so use it directly.
65 + kv_cache = attn_layer.kv_cache
66 +
67 + if kv_cache.numel() == 0:
68 + output.zero_()
69 + return
70 +
71 + assert kv_cache.dim() == 5, (
72 + f"Expected kv_cache to have 5 dims, got {tuple(kv_cache.shape)}"
73 + )
74 +
75 + rope_norm = _hpc_rope_norm_instances[layer_name]
76 + rope_norm._forward_impl(qkv, kv_cache, attn_metadata, attn_layer, output)
77 +
78 +
79 +def hpc_rope_norm_forward_fake(
80 + qkv: torch.Tensor,
81 + output: torch.Tensor,
82 + layer_name: str,
83 +) -> None:
84 + """Fake impl for torch.compile trace; output is a mutated arg."""
85 + return
86 +
87 +
88 +direct_register_custom_op(
89 + op_name="hpc_rope_norm_forward",
90 + op_func=hpc_rope_norm_forward,
91 + mutates_args=["output"],
92 + fake_impl=hpc_rope_norm_forward_fake,
93 +)
94 +
95 +
96 +@CustomOp.register("hpc_rope_norm")
97 +class HpcRopeNorm(CustomOp, HpcModule):
98 + """HPC fused RoPE + QK-Norm + KV-Cache-Write (+ optional FP8 Q quant).
99 +
100 + Registered as a sub-module in model layers (e.g. HunYuanAttention).
101 + Norm weights are extracted from fallback norm modules via
102 + process_weights_after_loading() after all weights are loaded.
103 +
104 + forward() is dispatched by CustomOp framework:
105 + - In compiled mode: forward_cuda() calls torch.ops.vllm.hpc_rope_norm_forward
106 + as a splitting point — internal Python control flow is opaque
107 + to torch.compile and not captured by CUDA Graph.
108 + - In eager/native mode: forward_native() falls back to forward_cuda().
109 + """
110 +
111 + def __init__(
112 + self,
113 + num_heads: int,
114 + num_kv_heads: int,
115 + head_dim: int,
116 + cos_sin_cache: torch.Tensor,
117 + use_qk_norm: bool,
118 + fallback_qnorm: torch.nn.Module | None,
119 + fallback_knorm: torch.nn.Module | None,
120 + kv_cache_dtype: str,
121 + layer_name: str,
122 + qk_norm_policy: QkNormPolicy = QkNormPolicy.ROPE_THEN_NORM,
123 + ) -> None:
124 + super().__init__()
125 + self.num_heads = num_heads
126 + self.num_kv_heads = num_kv_heads
127 + self.head_dim = head_dim
128 +
129 + self.use_qk_norm = use_qk_norm
130 +
131 + self.q_size = num_heads * head_dim
132 + self.kv_size = num_kv_heads * head_dim
133 +
134 + # Register as a non-persistent buffer so it participates in sleep
135 + # level-2 save/restore (CuMemAllocator) but is excluded from the
136 + # checkpoint state_dict.
137 + self.register_buffer("cos_sin_cache", cos_sin_cache.float(), persistent=False)
138 +
139 + self.fallback_qnorm = fallback_qnorm
140 + self.fallback_knorm = fallback_knorm
141 +
142 + self.head_per_group = num_heads // num_kv_heads
143 +
144 + # Pre-allocate norm weight tensors as Parameters so they are tracked by
145 + # CuMemAllocator (for sleep/wake_up) and have stable addresses for CUDA
146 + # Graph replay. process_weights_after_loading() updates them inplace via
147 + # copy_() so refit does not invalidate captured graph tensor pointers.
148 + # Shape is [head_dim] to match the HPC kernel's q/k_norm_weight layout.
149 + if use_qk_norm and fallback_qnorm is not None:
150 + self.qnorm_weight: torch.nn.Parameter | None = torch.nn.Parameter(
151 + torch.empty(head_dim, dtype=torch.float32),
152 + requires_grad=False,
153 + )
154 + else:
155 + self.qnorm_weight = None
156 + if use_qk_norm and fallback_knorm is not None:
157 + self.knorm_weight: torch.nn.Parameter | None = torch.nn.Parameter(
158 + torch.empty(head_dim, dtype=torch.float32),
159 + requires_grad=False,
160 + )
161 + else:
162 + self.knorm_weight = None
163 +
164 + self.use_fp8 = "fp8" in kv_cache_dtype
165 + # The RMSNorm/RoPE ordering is model dependent (e.g. HunYuan V3 applies
166 + # QK-Norm before RoPE -> NORM_THEN_ROPE), so it is supplied by the
167 + # caller. When QK-Norm is disabled the policy is forced to NONE.
168 + self.qk_norm_policy = qk_norm_policy if use_qk_norm else QkNormPolicy.NONE
169 +
170 + # Register layer_name + add self to the global instance registry so the
171 + # module-level custom op (hpc_rope_norm_forward) can route back here.
172 + self.layer_name: str | None = None
173 + self.register_layer_name(layer_name)
174 +
175 + @classmethod
176 + def support(
177 + cls,
178 + num_heads: int,
179 + num_kv_heads: int,
180 + head_dim: int,
181 + kv_cache_dtype: str,
182 + ) -> bool:
183 + """Check whether HpcRopeNorm is supported for the given config."""
184 + # HpcRopeNorm is only enabled together with the HPC attention backend.
185 + vllm_config = get_current_vllm_config_or_none()
186 + if (
187 + vllm_config is None
188 + or vllm_config.attention_config.backend != AttentionBackendEnum.HPC_ATTN
189 + ):
190 + return False
191 +
192 + if kv_cache_dtype not in ("fp8_e4m3", "auto"):
193 + logger.warning_once(
194 + f"hpc rope_norm not support kv_cache_dtype:{kv_cache_dtype}, "
195 + "only support fp8_e4m3, bfloat16"
196 + )
197 + return False
198 +
199 + if head_dim not in (128,):
200 + logger.warning_once("hpc rope_norm only support head_dim == 128.")
201 + return False
202 +
203 + head_per_group = num_heads // num_kv_heads
204 + if head_per_group not in (4, 8):
205 + logger.warning_once("hpc rope_norm only support head_per_group in [4, 8].")
206 + return False
207 +
208 + logger.info_once("enable hpc rope_norm")
209 + return True
210 +
211 + def process_weights_after_loading(self, model: torch.nn.Module = None) -> None:
212 + """Copy norm weights (float32) from fallback norm modules inplace.
213 +
214 + Uses copy_() to preserve tensor addresses for CUDA Graph / refit
215 + compatibility. Called by the model's load_weights() after all weights
216 + are loaded (and generically from the model loader for DummyModelLoader
217 + / sleep-wake_up reload paths).
218 + """
219 + if self.use_qk_norm:
220 + if self.fallback_qnorm is not None and self.qnorm_weight is not None:
221 + self.qnorm_weight.data.copy_(self.fallback_qnorm.weight.data.float())
222 + if self.fallback_knorm is not None and self.knorm_weight is not None:
223 + self.knorm_weight.data.copy_(self.fallback_knorm.weight.data.float())
224 +
225 + def register_layer_name(self, layer_name: str) -> None:
226 + """Register layer_name and add self to the global registry.
227 +
228 + The global registry is needed because the bottom-level torch op
229 + (hpc_rope_norm_forward) is a module-level function and needs to
230 + route back to the correct instance via layer_name.
231 + """
232 + self.layer_name = layer_name
233 + _hpc_rope_norm_instances[layer_name] = self
234 + logger.debug(
235 + "[rope_norm] registered HpcRopeNorm for layer: %s",
236 + layer_name,
237 + )
238 +
239 + def forward_native(
240 + self,
241 + qkv: torch.Tensor,
242 + layer_name: str,
243 + ) -> torch.Tensor:
244 + """Native fallback path: delegates to forward_cuda().
245 +
246 + For now, the default native path will use CUDA backend path.
247 + Other platforms may override via OOT registration.
248 + """
249 + return self.forward_cuda(qkv, layer_name)
250 +
251 + def forward_cuda(
252 + self,
253 + qkv: torch.Tensor,
254 + layer_name: str,
255 + ) -> torch.Tensor:
256 + """CUDA path: invoke the torch custom op as a compile splitting point."""
257 + num_tokens = qkv.shape[0]
258 + output = torch.empty(
259 + (num_tokens, self.num_heads, self.head_dim),
260 + dtype=torch.float8_e4m3fn if self.use_fp8 else qkv.dtype,
261 + device=qkv.device,
262 + )
263 +
264 + torch.ops.vllm.hpc_rope_norm_forward(qkv, output, layer_name)
265 + return output
266 +
267 + def _forward_impl(
268 + self,
269 + qkv: torch.Tensor,
270 + kv_cache: torch.Tensor,
271 + attn_metadata: HpcAttnMetadata,
272 + attn_layer: torch.nn.Module,
273 + output: torch.Tensor,
274 + ) -> None:
275 + """Actual forward logic called by the custom op.
276 +
277 + Writes processed q into *output* and attaches extra params
278 + (e.g. FP8 scales) to *attn_layer* as attributes.
279 + """
280 + import hpc
281 +
282 + num_actual_tokens = attn_metadata.num_actual_tokens
283 + num_prefill_reqs = attn_metadata.num_prefills
284 + num_decode_reqs = attn_metadata.num_decodes
285 + num_decode_tokens = attn_metadata.num_decode_tokens
286 +
287 + qkv = qkv[:num_actual_tokens]
288 +
289 + num_prefill_tokens = num_actual_tokens - num_decode_tokens
290 +
291 + # KV cache for the FP8 path is stored as uint8; view it as fp8 so the
292 + # rope_norm_store_kv_fp8 kernel can write quantized K/V in-place.
293 + if self.use_fp8:
294 + kv_cache = kv_cache.view(torch.float8_e4m3fn)
295 +
296 + # Per-tensor K/V scales (shape [1]) used by the FP8 kernel.
297 + k_scale = attn_layer._k_scale.reshape(1)
298 + v_scale = attn_layer._v_scale.reshape(1)
299 +
300 + q_norm_weight = (
301 + self.qnorm_weight if self.qk_norm_policy != QkNormPolicy.NONE else None
302 + )
303 + k_norm_weight = (
304 + self.knorm_weight if self.qk_norm_policy != QkNormPolicy.NONE else None
305 + )
306 +
307 + # Dynamic per-token-per-head Q quant + per-tensor K/V (dqskv).
308 + # rope_norm_store_kv_fp8 is registered as a torch op whose ``quant_policy``
309 + # argument is typed as ``int``; pybind cannot cast the hpc.QuantType enum
310 + # automatically, so pass its integer ``.value``.
311 + QUANT_POLICY_DQSKV = hpc.QuantType.QPERTOKEN_PERHEAD_KPERTENSOR_VPERTENSOR.value
312 +
313 + # --- Prefill ---
314 + if num_prefill_reqs > 0:
315 + seq_lens_prefill = attn_metadata.seq_lens[num_decode_reqs:]
316 + cu_seqlens_prefill = attn_metadata.qo_indptr
317 + max_seqlens = attn_metadata.max_query_len
318 + block_table_prefill = attn_metadata.block_table_tensor[num_decode_reqs:]
319 + qkv_prefill = qkv[num_decode_tokens:]
320 + out_q_prefill = output[
321 + num_decode_tokens : num_decode_tokens + num_prefill_tokens
322 + ]
323 +
324 + if self.use_fp8:
325 + _, q_scale, split_k_flag = hpc.rope_norm_store_kv_fp8(
326 + key_cache=kv_cache[:, 0],
327 + value_cache=kv_cache[:, 1],
328 + qkv=qkv_prefill,
329 + cos_sin=self.cos_sin_cache,
330 + num_seqlen_per_req=seq_lens_prefill,
331 + q_index=cu_seqlens_prefill,
332 + kvcache_indices=block_table_prefill,
333 + is_prefill=True,
334 + k_scale=k_scale,
335 + v_scale=v_scale,
336 + quant_policy=QUANT_POLICY_DQSKV,
337 + max_seqlens=max_seqlens,
338 + q_norm_weight=q_norm_weight,
339 + k_norm_weight=k_norm_weight,
340 + qk_norm_policy=self.qk_norm_policy,
341 + out_q=out_q_prefill,
342 + )
343 + attn_metadata.hpc_prefill_q_scale = q_scale
344 + else:
345 + hpc.rope_norm_store_kv(
346 + kv_cache[:, 0],
347 + kv_cache[:, 1],
348 + qkv_prefill,
349 + self.cos_sin_cache,
350 + seq_lens_prefill,
351 + cu_seqlens_prefill,
352 + block_table_prefill,
353 + True, # is_prefill
354 + q_norm_weight=q_norm_weight,
355 + k_norm_weight=k_norm_weight,
356 + out_q=out_q_prefill,
357 + qk_norm_policy=self.qk_norm_policy,
358 + )
359 +
360 + # --- Decode ---
361 + if num_decode_reqs > 0:
362 + num_seq_kvcache = attn_metadata.seq_lens[:num_decode_reqs]
363 + block_table_decode = attn_metadata.block_table_tensor[:num_decode_reqs]
364 + qkv_decode = qkv[:num_decode_tokens]
365 + # Single-token decode: q_index is the per-request prefix sum
366 + # [0, 1, ..., num_decode_reqs].
367 + qo_indptr_decode = torch.arange(
368 + num_decode_reqs + 1, dtype=torch.int32, device=qkv.device
369 + )
370 + out_q_decode = output[:num_decode_tokens]
371 +
372 + if self.use_fp8:
373 + _, q_scale, split_k_flag = hpc.rope_norm_store_kv_fp8(
374 + key_cache=kv_cache[:, 0],
375 + value_cache=kv_cache[:, 1],
376 + qkv=qkv_decode,
377 + cos_sin=self.cos_sin_cache,
378 + num_seqlen_per_req=num_seq_kvcache,
379 + q_index=qo_indptr_decode,
380 + kvcache_indices=block_table_decode,
381 + is_prefill=False,
382 + k_scale=k_scale,
383 + v_scale=v_scale,
384 + quant_policy=QUANT_POLICY_DQSKV,
385 + max_seqlens=1,
386 + q_norm_weight=q_norm_weight,
387 + k_norm_weight=k_norm_weight,
388 + qk_norm_policy=self.qk_norm_policy,
389 + out_q=out_q_decode,
390 + )
391 + attn_metadata.hpc_decode_q_scale = q_scale
392 + if split_k_flag is not None:
393 + attn_metadata.hpc_split_k_flag = split_k_flag
394 + else:
395 + hpc.rope_norm_store_kv(
396 + kv_cache[:, 0],
397 + kv_cache[:, 1],
398 + qkv_decode,
399 + self.cos_sin_cache,
400 + num_seq_kvcache,
401 + qo_indptr_decode,
402 + block_table_decode,
403 + False, # is_prefill
404 + q_norm_weight=q_norm_weight,
405 + k_norm_weight=k_norm_weight,
406 + out_q=out_q_decode,
407 + qk_norm_policy=self.qk_norm_policy,
408 + )
vllm/model_executor/model_loader/utils.py
+10
@@ -20,6 +20,7 @@ from vllm.model_executor.layers.attention import (
20 MLAAttention,
21 MMEncoderAttention,
22 )
23 +from vllm.model_executor.layers.hpc import HpcModule
24 from vllm.model_executor.layers.quantization.base_config import (
25 QuantizationConfig,
26 QuantizeMethodBase,
@@ -125,6 +126,15 @@ def process_weights_after_loading(
126 with device_loading_context(module, target_device):
127 module.process_weights_after_loading(model_config.dtype)
128
129 + # Process HPC modules (HpcRopeNorm, etc.) that rely on
130 + # process_weights_after_loading being called from the model's
131 + # load_weights(). When using DummyModelLoader (e.g. profiling or
132 + # sleep/wake_up reload), the model's load_weights() is not called, so we
133 + # must handle HPC modules here generically.
134 + for _, module in model.named_modules():
135 + if isinstance(module, HpcModule):
136 + module.process_weights_after_loading(model)
137 +
138 # Needed for torchao model reloading via model.reload_weights
139 # @kylesayrs @jerryzh168 this can be removed if callers move to `reload_weights`
140 if model_config.quantization == "torchao":
vllm/model_executor/models/hy_v3.py
+51 -13
@@ -48,6 +48,7 @@ from vllm.model_executor.layers.fused_moe import (
48 GateLinear,
49 fused_moe_make_expert_params_mapping,
50 )
51 +from vllm.model_executor.layers.hpc import HpcRopeNorm, QkNormPolicy
52 from vllm.model_executor.layers.layernorm import RMSNorm
53 from vllm.model_executor.layers.linear import (
54 MergedColumnParallelLinear,
@@ -234,6 +235,7 @@ class HYV3Attention(nn.Module):
235 dual_chunk_attention_config: dict[str, Any] | None = None,
236 ) -> None:
237 super().__init__()
238 + self.dtype = torch.get_default_dtype()
239 self.hidden_size = hidden_size
240 tp_size = get_tensor_model_parallel_world_size()
241 self.total_num_heads = num_heads
@@ -276,11 +278,18 @@ class HYV3Attention(nn.Module):
278 quant_config=quant_config,
279 prefix=f"{prefix}.o_proj",
280 )
281 + # When the HPC fused RoPE+QK-Norm path is enabled, the RoPE cos/sin
282 + # cache must be float32 to match the HPC kernel's expectations.
283 + kv_cache_dtype = cache_config.cache_dtype if cache_config else "auto"
284 + rope_support = HpcRopeNorm.support(
285 + self.num_heads, self.num_kv_heads, self.head_dim, kv_cache_dtype
286 + )
287 self.rotary_emb = get_rope(
288 self.head_dim,
289 max_position=max_position_embeddings,
290 rope_parameters=rope_parameters,
291 is_neox_style=True,
292 + dtype=torch.float32 if rope_support else torch.get_default_dtype(),
293 )
294 self.attn = Attention(
295 self.num_heads,
@@ -295,6 +304,27 @@ class HYV3Attention(nn.Module):
304 self.q_norm = RMSNorm(self.head_dim, rms_norm_eps)
305 self.k_norm = RMSNorm(self.head_dim, rms_norm_eps)
306
307 + # HPC fused RoPE + QK-Norm + KV-Cache-Write (+ optional FP8 Q quant).
308 + # HunYuan V3 applies QK-Norm *before* RoPE, so NORM_THEN_ROPE.
309 + self.hpc_rope_norm: HpcRopeNorm | None = None
310 + if rope_support:
311 + self.hpc_rope_norm = HpcRopeNorm(
312 + num_heads=self.num_heads,
313 + num_kv_heads=self.num_kv_heads,
314 + head_dim=self.head_dim,
315 + cos_sin_cache=self.rotary_emb.cos_sin_cache,
316 + use_qk_norm=self.use_qk_norm,
317 + fallback_qnorm=self.q_norm if self.use_qk_norm else None,
318 + fallback_knorm=self.k_norm if self.use_qk_norm else None,
319 + kv_cache_dtype=kv_cache_dtype,
320 + layer_name=self.attn.layer_name,
321 + qk_norm_policy=QkNormPolicy.NORM_THEN_ROPE,
322 + )
323 + # FP8 Q is produced by HpcRopeNorm, so the attention layer must not
324 + # re-quantize the query.
325 + if self.hpc_rope_norm.use_fp8 and hasattr(self.attn, "query_quant"):
326 + self.attn.query_quant = None
327 +
328 def forward(
329 self,
330 positions: torch.Tensor,
@@ -303,20 +333,28 @@ class HYV3Attention(nn.Module):
333 qkv, _ = self.qkv_proj(hidden_states)
334 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
335 output_shape = None
306 - if self.use_qk_norm:
307 - q_by_head = q.view(
308 - *q.shape[:-1], q.shape[-1] // self.head_dim, self.head_dim
309 - )
310 - q_by_head = self.q_norm(q_by_head)
311 - q = q_by_head.view(q.shape)
336 + if self.hpc_rope_norm is not None:
337 + # HPC handles QK-Norm + RoPE + KV-cache write (+ optional FP8 Q
338 + # quant) internally and returns the processed query. K/V are
339 + # written into the paged cache by the fused op.
340 + q = self.hpc_rope_norm(qkv, self.attn.layer_name)
341 + q = q.view(-1, self.num_heads * self.head_dim)
342 + attn_output = self.attn(q, k, v, output_shape, self.dtype)
343 + else:
344 + if self.use_qk_norm:
345 + q_by_head = q.view(
346 + *q.shape[:-1], q.shape[-1] // self.head_dim, self.head_dim
347 + )
348 + q_by_head = self.q_norm(q_by_head)
349 + q = q_by_head.view(q.shape)
350
313 - k_by_head = k.view(
314 - *k.shape[:-1], k.shape[-1] // self.head_dim, self.head_dim
315 - )
316 - k_by_head = self.k_norm(k_by_head)
317 - k = k_by_head.view(k.shape)
318 - q, k = self.rotary_emb(positions, q, k)
319 - attn_output = self.attn(q, k, v, output_shape)
351 + k_by_head = k.view(
352 + *k.shape[:-1], k.shape[-1] // self.head_dim, self.head_dim
353 + )
354 + k_by_head = self.k_norm(k_by_head)
355 + k = k_by_head.view(k.shape)
356 + q, k = self.rotary_emb(positions, q, k)
357 + attn_output = self.attn(q, k, v, output_shape)
358 attn_output = attn_output.view(q.shape[0], -1)
359 output, _ = self.o_proj(attn_output)
360 return output
vllm/v1/attention/backends/hpc_attn.py new
+469
@@ -0,0 +1,469 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""HPC Attention Backend.
4 +
5 +Pure attention (prefill + decode), without RoPE or RMSNorm.
6 +Independent metadata / builder; KV cache layout is NHD:
7 +(num_blocks, 2, block_size, num_kv_heads, head_size).
8 +"""
9 +
10 +import importlib.util
11 +from dataclasses import dataclass
12 +from typing import ClassVar
13 +
14 +import torch
15 +from typing_extensions import override
16 +
17 +from vllm.config import VllmConfig
18 +from vllm.config.cache import CacheDType
19 +from vllm.logger import init_logger
20 +from vllm.platforms import current_platform
21 +from vllm.platforms.interface import DeviceCapability
22 +from vllm.v1.attention.backend import (
23 + AttentionBackend,
24 + AttentionCGSupport,
25 + AttentionImpl,
26 + AttentionMetadata,
27 + AttentionMetadataBuilder,
28 + AttentionType,
29 + CommonAttentionMetadata,
30 + MultipleOf,
31 +)
32 +from vllm.v1.attention.backends.utils import (
33 + KVCacheLayoutType,
34 + get_per_layer_parameters,
35 + infer_global_hyperparameters,
36 + split_decodes_and_prefills,
37 +)
38 +from vllm.v1.kv_cache_interface import AttentionSpec
39 +
40 +logger = init_logger(__name__)
41 +
42 +FP8_DTYPE = current_platform.fp8_dtype()
43 +
44 +
45 +def _get_fp8_dtype_for_kv_cache(kv_cache_dtype: str) -> torch.dtype:
46 + """Return the torch FP8 dtype for the given kv_cache_dtype string."""
47 + if kv_cache_dtype in ("fp8", "fp8_e4m3"):
48 + return torch.float8_e4m3fn
49 + elif kv_cache_dtype == "fp8_e5m2":
50 + return torch.float8_e5m2
51 + else:
52 + raise ValueError(f"Unrecognized FP8 dtype: {kv_cache_dtype}")
53 +
54 +
55 +@dataclass
56 +class HpcAttnMetadata(AttentionMetadata):
57 + """Metadata required by the HPC attention kernel."""
58 +
59 + num_actual_tokens: int
60 + num_decodes: int
61 + num_decode_tokens: int
62 + num_prefills: int
63 + num_prefill_tokens: int
64 + max_query_len: int
65 +
66 + slot_mapping: torch.Tensor
67 + """Slot mapping for KV cache writes. shape = [num_actual_tokens]"""
68 +
69 + seq_lens: torch.Tensor
70 + """KV cache length per request. shape = [batch_size]"""
71 +
72 + block_table_tensor: torch.Tensor
73 + """Paged KV-cache block table.
74 + shape = [batch_size, max_num_blocks_per_seq]"""
75 +
76 + qo_indptr: torch.Tensor | None = None
77 + """Cumulative query lengths for prefill requests (GPU tensor).
78 + shape = [num_prefills + 1]. None when num_prefills == 0."""
79 +
80 + # --- HPC RopeNorm pass-through fields ---
81 + # Set by HpcRopeNorm._forward_impl(); consumed & reset by
82 + # HpcAttentionImpl.forward(). Defaults are safe for the standard
83 + # (non-RopeNorm) path and for profiling runs (attn_metadata=None).
84 + hpc_kv_written: bool = False
85 + """True when HpcRopeNorm already wrote KV cache."""
86 + hpc_prefill_q_scale: torch.Tensor | None = None
87 + """FP8 per-token-per-head Q scale for prefill (from RopeNorm)."""
88 + hpc_decode_q_scale: torch.Tensor | None = None
89 + """FP8 per-token-per-head Q scale for decode (from RopeNorm)."""
90 + hpc_split_k_flag: torch.Tensor | None = None
91 + """Split-K flag tensor for FP8 decode (from RopeNorm)."""
92 +
93 +
94 +class HpcAttnMetadataBuilder(AttentionMetadataBuilder[HpcAttnMetadata]):
95 + """Build HpcAttnMetadata from CommonAttentionMetadata."""
96 +
97 + _cudagraph_support = AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
98 + reorder_batch_threshold: int = 1
99 +
100 + def __init__(
101 + self,
102 + kv_cache_spec: AttentionSpec,
103 + layer_names: list[str],
104 + vllm_config: VllmConfig,
105 + device: torch.device,
106 + ):
107 + super().__init__(kv_cache_spec, layer_names, vllm_config, device)
108 + self.model_config = vllm_config.model_config
109 + self.cache_config = vllm_config.cache_config
110 +
111 + self.num_qo_heads = self.model_config.get_num_attention_heads(
112 + vllm_config.parallel_config
113 + )
114 + self.num_kv_heads = kv_cache_spec.num_kv_heads
115 + self.head_dim = kv_cache_spec.head_size
116 + self.page_size = kv_cache_spec.block_size
117 +
118 + self.cache_dtype = self.cache_config.cache_dtype
119 +
120 + self.global_hyperparameters = infer_global_hyperparameters(
121 + get_per_layer_parameters(vllm_config, layer_names, HpcAttentionImpl)
122 + )
123 +
124 + @override # type: ignore[misc]
125 + @classmethod
126 + def get_cudagraph_support(
127 + cls: type["HpcAttnMetadataBuilder"],
128 + vllm_config: VllmConfig,
129 + kv_cache_spec: AttentionSpec,
130 + ) -> AttentionCGSupport:
131 + return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
132 +
133 + def build(
134 + self,
135 + common_prefix_len: int,
136 + common_attn_metadata: CommonAttentionMetadata,
137 + fast_build: bool = False,
138 + ) -> HpcAttnMetadata:
139 + """Build HpcAttnMetadata from CommonAttentionMetadata."""
140 + num_actual_tokens = common_attn_metadata.num_actual_tokens
141 +
142 + num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
143 + split_decodes_and_prefills(
144 + common_attn_metadata,
145 + decode_threshold=self.reorder_batch_threshold,
146 + require_uniform=False,
147 + )
148 + )
149 +
150 + seq_lens = common_attn_metadata.seq_lens
151 + block_table_tensor = common_attn_metadata.block_table_tensor
152 + slot_mapping = common_attn_metadata.slot_mapping
153 + max_query_len = common_attn_metadata.max_query_len
154 +
155 + qo_indptr = None
156 + if num_prefills > 0:
157 + qo_indptr_cpu = common_attn_metadata.query_start_loc_cpu
158 + prefill_start = num_decodes
159 + qo_indptr_prefill_cpu = (
160 + qo_indptr_cpu[prefill_start:] - qo_indptr_cpu[prefill_start]
161 + )
162 + qo_indptr = qo_indptr_prefill_cpu.to(self.device, non_blocking=True)
163 +
164 + return HpcAttnMetadata(
165 + num_actual_tokens=num_actual_tokens,
166 + num_decodes=num_decodes,
167 + num_decode_tokens=num_decode_tokens,
168 + num_prefills=num_prefills,
169 + num_prefill_tokens=num_prefill_tokens,
170 + max_query_len=max_query_len,
171 + slot_mapping=slot_mapping,
172 + seq_lens=seq_lens,
173 + block_table_tensor=block_table_tensor,
174 + qo_indptr=qo_indptr,
175 + hpc_kv_written=True,
176 + hpc_prefill_q_scale=None,
177 + hpc_decode_q_scale=None,
178 + hpc_split_k_flag=None,
179 + )
180 +
181 +
182 +class HpcAttentionBackend(AttentionBackend):
183 + """HPC attention backend (pure attention, no RoPE/Norm).
184 +
185 + KV cache layout: NHD (num_blocks, 2, block_size, num_kv_heads, head_size).
186 + """
187 +
188 + accept_output_buffer: bool = True
189 + supported_dtypes: ClassVar[list[torch.dtype]] = [
190 + torch.float16,
191 + torch.bfloat16,
192 + ]
193 + supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
194 + "auto",
195 + "fp8_e4m3",
196 + ]
197 +
198 + # Avoid attention abstracted method call cache insert
199 + forward_includes_kv_cache_update: bool = True
200 +
201 + @staticmethod
202 + def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
203 + return [64]
204 +
205 + @staticmethod
206 + def get_name() -> str:
207 + return "HPC_ATTN"
208 +
209 + @staticmethod
210 + def get_impl_cls() -> type["HpcAttentionImpl"]:
211 + return HpcAttentionImpl
212 +
213 + @staticmethod
214 + def get_builder_cls() -> type["HpcAttnMetadataBuilder"]:
215 + return HpcAttnMetadataBuilder
216 +
217 + @staticmethod
218 + def get_kv_cache_shape(
219 + num_blocks: int,
220 + block_size: int,
221 + num_kv_heads: int,
222 + head_size: int,
223 + cache_dtype_str: str = "auto",
224 + ) -> tuple[int, ...]:
225 + return (num_blocks, 2, block_size, num_kv_heads, head_size)
226 +
227 + @staticmethod
228 + def get_kv_cache_stride_order(
229 + include_num_layers_dimension: bool = False,
230 + ) -> tuple[int, ...]:
231 + if include_num_layers_dimension:
232 + return (1, 0, 2, 3, 4, 5)
233 + return (0, 1, 2, 3, 4)
234 +
235 + @classmethod
236 + def get_supported_head_sizes(cls) -> list[int]:
237 + return [128]
238 +
239 + @classmethod
240 + def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
241 + return capability >= DeviceCapability(9, 0)
242 +
243 + @classmethod
244 + def supports_kv_cache_dtype(cls, kv_cache_dtype: "CacheDType | None") -> bool:
245 + if kv_cache_dtype is None:
246 + return True
247 + return kv_cache_dtype in cls.supported_kv_cache_dtypes
248 +
249 + @classmethod
250 + def get_required_kv_cache_layout(cls) -> KVCacheLayoutType | None:
251 + return "NHD"
252 +
253 +
254 +class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
255 + """HPC pure attention implementation (no RoPE/Norm).
256 +
257 + Constraints:
258 + - head_dim == 128
259 + - num_heads // num_kv_heads in {4, 8}
260 + - kv_cache_dtype in {"auto", "fp8_e4m3"}
261 + """
262 +
263 + def __init__(
264 + self,
265 + num_heads: int,
266 + head_size: int,
267 + scale: float,
268 + num_kv_heads: int,
269 + alibi_slopes: list[float] | None = None,
270 + sliding_window: int | None = None,
271 + kv_cache_dtype: str = "auto",
272 + logits_soft_cap: float | None = None,
273 + attn_type: str = AttentionType.DECODER,
274 + kv_sharing_target_layer_name: str | None = None,
275 + ) -> None:
276 + if importlib.util.find_spec("hpc") is None:
277 + raise ImportError(
278 + "HPC attention requires the hpc module to be installed. "
279 + "Please install it from https://github.com/Tencent/hpc-ops"
280 + )
281 + if attn_type != AttentionType.DECODER:
282 + raise NotImplementedError("HPC attention only supports decoder attention")
283 + if alibi_slopes is not None:
284 + raise NotImplementedError("HPC attention does not support ALiBi")
285 + if logits_soft_cap is not None:
286 + raise NotImplementedError("HPC attention does not support logits_soft_cap")
287 +
288 + if head_size != 128:
289 + raise ValueError(
290 + f"HPC attention only supports head_dim=128, got {head_size}"
291 + )
292 +
293 + num_queries_per_kv = num_heads // num_kv_heads
294 + if num_queries_per_kv not in (4, 8):
295 + raise ValueError(
296 + f"HPC attention only supports head_per_group in {{4, 8}}, "
297 + f"got {num_queries_per_kv} "
298 + f"(num_heads={num_heads}, num_kv_heads={num_kv_heads})"
299 + )
300 +
301 + if kv_cache_dtype not in ("auto", "fp8_e4m3"):
302 + raise ValueError(
303 + f"HPC attention only supports kv_cache_dtype 'auto' or "
304 + f"'fp8_e4m3', got '{kv_cache_dtype}'"
305 + )
306 +
307 + self.num_heads = num_heads
308 + self.head_size = head_size
309 + self.scale = float(scale)
310 + self.num_kv_heads = num_kv_heads
311 + self.kv_cache_dtype = kv_cache_dtype
312 + self.kv_sharing_target_layer_name = kv_sharing_target_layer_name
313 +
314 + self.num_queries_per_kv = num_queries_per_kv
315 +
316 + if sliding_window is None:
317 + self.sliding_window = (-1, -1)
318 + else:
319 + self.sliding_window = (sliding_window - 1, 0)
320 +
321 + self.use_fp8 = kv_cache_dtype == "fp8_e4m3"
322 +
323 + self.supports_quant_query_input = False
324 + self.splitk = True
325 +
326 + def forward(
327 + self,
328 + layer: torch.nn.Module,
329 + query: torch.Tensor,
330 + key: torch.Tensor,
331 + value: torch.Tensor,
332 + kv_cache: torch.Tensor,
333 + attn_metadata: HpcAttnMetadata | None,
334 + output: torch.Tensor | None = None,
335 + output_scale: torch.Tensor | None = None,
336 + output_block_scale: torch.Tensor | None = None,
337 + ) -> torch.Tensor:
338 + """HPC attention forward (standard vLLM backend interface).
339 +
340 + Two modes:
341 + 1. Standard: upstream handles RoPE/Norm; this backend writes KV + attn.
342 + 2. HpcRopeNorm: fused op already did RoPE/Norm/KV-Write/Q-Quant;
343 + extra params passed via attn_metadata.hpc_* fields.
344 + """
345 + import hpc
346 +
347 + assert output is not None, "Output tensor must be provided."
348 + assert output_scale is None, "HPC attention does not support fused output quant"
349 + assert output_block_scale is None
350 +
351 + if attn_metadata is None:
352 + return output.fill_(0)
353 +
354 + hpc_kv_written = attn_metadata.hpc_kv_written
355 + hpc_prefill_q_scale = attn_metadata.hpc_prefill_q_scale
356 + hpc_decode_q_scale = attn_metadata.hpc_decode_q_scale
357 + hpc_split_k_flag = attn_metadata.hpc_split_k_flag
358 +
359 + num_actual_tokens = attn_metadata.num_actual_tokens
360 + num_prefill_reqs = attn_metadata.num_prefills
361 + num_decode_reqs = attn_metadata.num_decodes
362 + num_decode_tokens = attn_metadata.num_decode_tokens
363 +
364 + # Write KV cache if not already done by HpcRopeNorm.
365 + if self.kv_sharing_target_layer_name is None and not hpc_kv_written:
366 + torch.ops._C_cache_ops.reshape_and_cache_flash(
367 + key,
368 + value,
369 + kv_cache[:, 0],
370 + kv_cache[:, 1],
371 + attn_metadata.slot_mapping,
372 + self.kv_cache_dtype,
373 + layer._k_scale,
374 + layer._v_scale,
375 + )
376 +
377 + if self.use_fp8:
378 + torch_dtype = _get_fp8_dtype_for_kv_cache(self.kv_cache_dtype)
379 + kv_cache = kv_cache.view(torch_dtype)
380 +
381 + if self.use_fp8:
382 + if not hpc_kv_written:
383 + raise RuntimeError(
384 + "HpcAttentionImpl: FP8 mode requires HpcRopeNorm. "
385 + "Ensure hpc_rope_norm is enabled or set "
386 + "kv_cache_dtype='auto' for bf16 mode."
387 + f" (layer={getattr(layer, 'layer_name', '?')})"
388 + )
389 + k_scale = layer._k_scale.reshape(1)
390 + v_scale = layer._v_scale.reshape(1)
391 +
392 + query = query[:num_actual_tokens]
393 + key = key[:num_actual_tokens]
394 + value = value[:num_actual_tokens]
395 + output_padded = output
396 + output = output[:num_actual_tokens]
397 +
398 + # --- Prefill ---
399 + if num_prefill_reqs > 0:
400 + seq_lens_prefill = attn_metadata.seq_lens[num_decode_reqs:]
401 + cu_seqlens_prefill = attn_metadata.qo_indptr
402 + max_seqlens = attn_metadata.max_query_len
403 + block_table_prefill = attn_metadata.block_table_tensor[num_decode_reqs:]
404 +
405 + q_prefill = query[num_decode_tokens:]
406 + output_prefill = output[num_decode_tokens:]
407 +
408 + if self.use_fp8:
409 + hpc.attention_with_kvcache_prefill_fp8(
410 + q_prefill,
411 + kv_cache[:, 0],
412 + kv_cache[:, 1],
413 + hpc_prefill_q_scale,
414 + k_scale,
415 + v_scale,
416 + cu_seqlens_prefill,
417 + block_table_prefill,
418 + seq_lens_prefill,
419 + max_seqlens,
420 + output=output_prefill,
421 + )
422 + else:
423 + hpc.attention_with_kvcache_prefill_bf16(
424 + q_prefill,
425 + kv_cache[:, 0],
426 + kv_cache[:, 1],
427 + cu_seqlens_prefill,
428 + block_table_prefill,
429 + seq_lens_prefill,
430 + max_seqlens,
431 + output=output_prefill,
432 + )
433 +
434 + # --- Decode ---
435 + if num_decode_reqs > 0:
436 + num_seq_kvcache = attn_metadata.seq_lens[:num_decode_reqs]
437 + block_table_decode = attn_metadata.block_table_tensor[:num_decode_reqs]
438 +
439 + q_decode = query[:num_decode_tokens]
440 + output_decode = output[:num_decode_tokens]
441 +
442 + if self.use_fp8:
443 + hpc.attention_decode_fp8(
444 + q_decode,
445 + kv_cache[:, 0],
446 + kv_cache[:, 1],
447 + block_table_decode,
448 + num_seq_kvcache,
449 + hpc_decode_q_scale,
450 + k_scale,
451 + v_scale,
452 + new_kv_included=True,
453 + splitk=self.splitk,
454 + split_flag=hpc_split_k_flag,
455 + output=output_decode,
456 + )
457 + else:
458 + hpc.attention_decode_bf16(
459 + q_decode,
460 + kv_cache[:, 0],
461 + kv_cache[:, 1],
462 + block_table_decode,
463 + num_seq_kvcache,
464 + output=output_decode,
465 + new_kv_included=True,
466 + splitk=self.splitk,
467 + )
468 +
469 + return output_padded
vllm/v1/attention/backends/registry.py
+6
@@ -103,6 +103,12 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
103 )
104 NO_ATTENTION = "vllm.v1.attention.backends.no_attention.NoAttentionBackend"
105 FLEX_ATTENTION = "vllm.v1.attention.backends.flex_attention.FlexAttentionBackend"
106 + # HPC Attention Backend:
107 + # powered by operators from https://github.com/Tencent/hpc-ops.
108 + # Only supported on NVIDIA Hopper GPUs (e.g. H20, H200),
109 + # currently limited to the Hy3 model,
110 + # and requires a block size of 64.
111 + HPC_ATTN = "vllm.v1.attention.backends.hpc_attn.HpcAttentionBackend"
112 ROCM_AITER_UNIFIED_ATTN = (
113 "vllm.v1.attention.backends.rocm_aiter_unified_attn."
114 "RocmAiterUnifiedAttentionBackend"