[Attention Backend] Extend hpc_ops dynamic-scheduled decode to bf16 (#32304)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Halcyon <56064364+VAthree@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Fable 5
Halcyon
parent
5656de2d9a
commit
8d6549bc40
+1
-1
@@ -342,7 +342,7 @@ FROM torch_deps AS hpc_ops_builder
|
|||||||
# HPC-Ops (https://github.com/Tencent/hpc-ops, MIT): fused attention / MoE /
|
# HPC-Ops (https://github.com/Tencent/hpc-ops, MIT): fused attention / MoE /
|
||||||
# RoPE kernels from the Tencent Hunyuan AI Infra team, consumed by the opt-in
|
# RoPE kernels from the Tencent Hunyuan AI Infra team, consumed by the opt-in
|
||||||
# hpc_ops attention and MoE runner backends.
|
# hpc_ops attention and MoE runner backends.
|
||||||
ARG HPC_OPS_COMMIT=2404f09766269d9533b66709f705c8eeb01421fc
|
ARG HPC_OPS_COMMIT=6e2ecede9d6d47b2e680e839cc7ad7422bc8d88b
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
|||||||
@@ -627,7 +627,7 @@ python3 -m sglang.launch_server \
|
|||||||
--trust-remote-code
|
--trust-remote-code
|
||||||
```
|
```
|
||||||
|
|
||||||
- HPC-Ops (MHA kernels from [HPC-Ops](https://github.com/Tencent/hpc-ops) by the Tencent Hunyuan AI Infra team; Hopper+, requires installing the `hpc` package from source, page size 64, bf16 or fp8_e4m3 KV cache, head_dim 128, q/kv head group 4 or 8)
|
- HPC-Ops (MHA kernels from [HPC-Ops](https://github.com/Tencent/hpc-ops) by the Tencent Hunyuan AI Infra team; Hopper (SM90) only, requires installing the `hpc` package from source, page size 64, bf16 or fp8_e4m3 KV cache, head_dim 128, q/kv head group 4 or 8)
|
||||||
```bash Command
|
```bash Command
|
||||||
python3 -m sglang.launch_server \
|
python3 -m sglang.launch_server \
|
||||||
--model Qwen/Qwen3-30B-A3B-Instruct-2507-FP8 \
|
--model Qwen/Qwen3-30B-A3B-Instruct-2507-FP8 \
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ token-major KV pool as a paged NHD cache ``(num_pages, page_size, num_kv_heads,
|
|||||||
head_dim)`` without any copy, so the only hard requirements are the kernel
|
head_dim)`` without any copy, so the only hard requirements are the kernel
|
||||||
constraints:
|
constraints:
|
||||||
|
|
||||||
- NVIDIA Hopper or newer (sm90+)
|
- NVIDIA Hopper (SM90 only; the kernels ship sm90a)
|
||||||
- ``--page-size 64``
|
- ``--page-size 64``
|
||||||
- bf16 model dtype; bf16 or fp8_e4m3 KV cache (the FP8 path additionally
|
- bf16 model dtype; bf16 or fp8_e4m3 KV cache (the FP8 path additionally
|
||||||
requires the model to run the fused QKNorm+RoPE+quant+StoreKV op via
|
requires the model to run the fused QKNorm+RoPE+quant+StoreKV op via
|
||||||
@@ -21,8 +21,8 @@ constraints:
|
|||||||
no logit cap, decoder-only attention
|
no logit cap, decoder-only attention
|
||||||
|
|
||||||
Note that the HPC-Ops kernels are currently tuned primarily for H20: on other
|
Note that the HPC-Ops kernels are currently tuned primarily for H20: on other
|
||||||
GPUs (H100/H200/B200, ...) the speedup over the default attention backend may
|
SM90 GPUs (H100/H200) the speedup over the default attention backend may be
|
||||||
be limited or absent.
|
limited or absent.
|
||||||
|
|
||||||
Enable it explicitly with ``--attention-backend hpc_ops``.
|
Enable it explicitly with ``--attention-backend hpc_ops``.
|
||||||
"""
|
"""
|
||||||
@@ -67,7 +67,7 @@ _REQUIRED_PAGE_SIZE = 64
|
|||||||
FP8_ROPE_SUPPORTED_HEAD_CONFIGS = ((8, 1), (64, 8))
|
FP8_ROPE_SUPPORTED_HEAD_CONFIGS = ((8, 1), (64, 8))
|
||||||
# Minimum tokens each SM processes per task in the dynamic-scheduled decode
|
# Minimum tokens each SM processes per task in the dynamic-scheduled decode
|
||||||
# path (matches the HPC-Ops default).
|
# path (matches the HPC-Ops default).
|
||||||
_FP8_DYNAMIC_SCHED_MIN_PROCESS_LEN = 512
|
_DYNAMIC_SCHED_MIN_PROCESS_LEN = 512
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
@functools.cache
|
||||||
@@ -76,6 +76,16 @@ def has_hpc_ops() -> bool:
|
|||||||
return importlib.util.find_spec("hpc") is not None
|
return importlib.util.find_spec("hpc") is not None
|
||||||
|
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def _bf16_decode_supports_task_map() -> bool:
|
||||||
|
"""hpc >= 6e2eced (PR #73) extends dynamic scheduling to bf16 decode."""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import hpc
|
||||||
|
|
||||||
|
return "task_map" in inspect.signature(hpc.attention_decode_bf16).parameters
|
||||||
|
|
||||||
|
|
||||||
class HPCOpsMetadata(msgspec.Struct):
|
class HPCOpsMetadata(msgspec.Struct):
|
||||||
"""Per-forward-batch metadata consumed by the HPC-Ops kernels."""
|
"""Per-forward-batch metadata consumed by the HPC-Ops kernels."""
|
||||||
|
|
||||||
@@ -101,7 +111,7 @@ class HPCOpsMetadata(msgspec.Struct):
|
|||||||
hpc_q_scale: Optional[torch.Tensor] = None
|
hpc_q_scale: Optional[torch.Tensor] = None
|
||||||
# Split-K flag tensor for FP8 decode. shape: [bs, num_kv_heads], int32
|
# Split-K flag tensor for FP8 decode. shape: [bs, num_kv_heads], int32
|
||||||
hpc_split_k_flag: Optional[torch.Tensor] = None
|
hpc_split_k_flag: Optional[torch.Tensor] = None
|
||||||
# Pre-scheduled decode task map (dynamic-scheduled FP8 decode); None falls
|
# Pre-scheduled decode task map (dynamic-scheduled decode); None falls
|
||||||
# back to the kernel's static split-K scheduling.
|
# back to the kernel's static split-K scheduling.
|
||||||
hpc_task_map: Optional[torch.Tensor] = None
|
hpc_task_map: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
@@ -122,6 +132,13 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
"Install it from https://github.com/Tencent/hpc-ops"
|
"Install it from https://github.com/Tencent/hpc-ops"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
major, minor = torch.cuda.get_device_capability()
|
||||||
|
if major != 9:
|
||||||
|
raise ValueError(
|
||||||
|
"The hpc_ops attention backend requires an SM90 (Hopper) GPU "
|
||||||
|
f"(the HPC-Ops kernels ship sm90a only), got sm{major}{minor}."
|
||||||
|
)
|
||||||
|
|
||||||
if model_runner.spec_algorithm.is_speculative():
|
if model_runner.spec_algorithm.is_speculative():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"The hpc_ops attention backend does not support speculative "
|
"The hpc_ops attention backend does not support speculative "
|
||||||
@@ -191,13 +208,15 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
# Fallback per-tensor KV scale for checkpoints without kv scales.
|
# Fallback per-tensor KV scale for checkpoints without kv scales.
|
||||||
self._ones_scale = torch.ones(1, dtype=torch.float32, device=self.device)
|
self._ones_scale = torch.ones(1, dtype=torch.float32, device=self.device)
|
||||||
|
|
||||||
# Dynamic-scheduled FP8 decode: the task workspace is sized by the
|
# Dynamic-scheduled decode (fp8 always; bf16 when the installed hpc
|
||||||
# decode CUDA-graph max batch size in init_cuda_graph_state (sizing it
|
# supports it): the task workspace is sized by the decode CUDA-graph
|
||||||
# by the full request-pool capacity would cost hundreds of MB); eager
|
# max batch size in init_cuda_graph_state (sizing it by the full
|
||||||
# decode batches beyond that fall back to static split-K.
|
# request-pool capacity would cost hundreds of MB); eager decode
|
||||||
|
# batches beyond that fall back to static split-K.
|
||||||
self.num_kv_heads = num_kv_heads
|
self.num_kv_heads = num_kv_heads
|
||||||
self._fp8_task_map: Optional[torch.Tensor] = None
|
self._dynamic_sched = self.use_fp8 or _bf16_decode_supports_task_map()
|
||||||
self._fp8_task_map_max_bs = 0
|
self._decode_task_map: Optional[torch.Tensor] = None
|
||||||
|
self._decode_task_map_max_bs = 0
|
||||||
|
|
||||||
# CUDA graph state (allocated in init_cuda_graph_state).
|
# CUDA graph state (allocated in init_cuda_graph_state).
|
||||||
self.decode_cuda_graph_metadata = {}
|
self.decode_cuda_graph_metadata = {}
|
||||||
@@ -239,27 +258,26 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
page_size=self.page_size,
|
page_size=self.page_size,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
self.use_fp8
|
self._decode_task_map is not None
|
||||||
and self._fp8_task_map is not None
|
|
||||||
and forward_batch.forward_mode.is_decode()
|
and forward_batch.forward_mode.is_decode()
|
||||||
and batch_size <= self._fp8_task_map_max_bs
|
and batch_size <= self._decode_task_map_max_bs
|
||||||
):
|
):
|
||||||
metadata.hpc_task_map = self._assign_fp8_decode_tasks(
|
metadata.hpc_task_map = self._assign_decode_tasks(
|
||||||
metadata.cache_seqlens_int32
|
metadata.cache_seqlens_int32
|
||||||
)
|
)
|
||||||
self.forward_metadata = metadata
|
self.forward_metadata = metadata
|
||||||
|
|
||||||
def init_cuda_graph_state(self, max_bs: int, max_num_tokens: int):
|
def init_cuda_graph_state(self, max_bs: int, max_num_tokens: int):
|
||||||
if self.use_fp8:
|
if self._dynamic_sched:
|
||||||
import hpc
|
import hpc
|
||||||
|
|
||||||
self._fp8_task_map = hpc.get_attention_decode_task_workspace(
|
self._decode_task_map = hpc.get_attention_decode_task_workspace(
|
||||||
max_bs,
|
max_bs,
|
||||||
self.max_context_len,
|
self.max_context_len,
|
||||||
self.num_kv_heads,
|
self.num_kv_heads,
|
||||||
min_process_len=_FP8_DYNAMIC_SCHED_MIN_PROCESS_LEN,
|
min_process_len=_DYNAMIC_SCHED_MIN_PROCESS_LEN,
|
||||||
)
|
)
|
||||||
self._fp8_task_map_max_bs = max_bs
|
self._decode_task_map_max_bs = max_bs
|
||||||
|
|
||||||
self.decode_cuda_graph_metadata = {
|
self.decode_cuda_graph_metadata = {
|
||||||
"cache_seqlens": torch.zeros(max_bs, dtype=torch.int32, device=self.device),
|
"cache_seqlens": torch.zeros(max_bs, dtype=torch.int32, device=self.device),
|
||||||
@@ -309,10 +327,10 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
max_seq_pages=self.max_num_pages,
|
max_seq_pages=self.max_num_pages,
|
||||||
page_size=self.page_size,
|
page_size=self.page_size,
|
||||||
)
|
)
|
||||||
if self.use_fp8 and self._fp8_task_map is not None:
|
if self._decode_task_map is not None:
|
||||||
# Recorded into the decode graph, so the task map is re-populated
|
# Recorded into the decode graph, so the task map is re-populated
|
||||||
# from the live seq_lens at every replay.
|
# from the live seq_lens at every replay.
|
||||||
metadata.hpc_task_map = self._assign_fp8_decode_tasks(
|
metadata.hpc_task_map = self._assign_decode_tasks(
|
||||||
metadata.cache_seqlens_int32
|
metadata.cache_seqlens_int32
|
||||||
)
|
)
|
||||||
self.forward_metadata = metadata
|
self.forward_metadata = metadata
|
||||||
@@ -479,7 +497,7 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
metadata.hpc_q_scale = q_scale
|
metadata.hpc_q_scale = q_scale
|
||||||
metadata.hpc_split_k_flag = split_k_flag
|
metadata.hpc_split_k_flag = split_k_flag
|
||||||
|
|
||||||
def _assign_fp8_decode_tasks(self, cache_seqlens: torch.Tensor) -> torch.Tensor:
|
def _assign_decode_tasks(self, cache_seqlens: torch.Tensor) -> torch.Tensor:
|
||||||
"""Populate the dynamic-scheduled decode task map from live KV lengths.
|
"""Populate the dynamic-scheduled decode task map from live KV lengths.
|
||||||
|
|
||||||
The scheduler pass bins every (request, kv_head, KV-tile) chunk into
|
The scheduler pass bins every (request, kv_head, KV-tile) chunk into
|
||||||
@@ -494,13 +512,13 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
# kernel launch with an invalid configuration.
|
# kernel launch with an invalid configuration.
|
||||||
hpc.assign_attention_decode_task(
|
hpc.assign_attention_decode_task(
|
||||||
cache_seqlens,
|
cache_seqlens,
|
||||||
self._fp8_task_map,
|
self._decode_task_map,
|
||||||
self.num_kv_heads,
|
self.num_kv_heads,
|
||||||
mtp=1,
|
mtp=1,
|
||||||
new_kv_included=True,
|
new_kv_included=True,
|
||||||
min_process_len=_FP8_DYNAMIC_SCHED_MIN_PROCESS_LEN,
|
min_process_len=_DYNAMIC_SCHED_MIN_PROCESS_LEN,
|
||||||
)
|
)
|
||||||
return self._fp8_task_map
|
return self._decode_task_map
|
||||||
|
|
||||||
def _take_fp8_scales(self, metadata: HPCOpsMetadata):
|
def _take_fp8_scales(self, metadata: HPCOpsMetadata):
|
||||||
"""Pop the per-layer FP8 scales written by the fused RoPE op."""
|
"""Pop the per-layer FP8 scales written by the fused RoPE op."""
|
||||||
@@ -619,6 +637,13 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
split_flag=split_k_flag,
|
split_flag=split_k_flag,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# Older hpc's bf16 decode has no task_map kwarg; the map is only
|
||||||
|
# ever assigned when the installed hpc supports it.
|
||||||
|
task_map_kwargs = (
|
||||||
|
{"task_map": metadata.hpc_task_map}
|
||||||
|
if metadata.hpc_task_map is not None
|
||||||
|
else {}
|
||||||
|
)
|
||||||
o = hpc.attention_decode_bf16(
|
o = hpc.attention_decode_bf16(
|
||||||
q.view(-1, layer.tp_q_head_num, layer.head_dim),
|
q.view(-1, layer.tp_q_head_num, layer.head_dim),
|
||||||
k_cache,
|
k_cache,
|
||||||
@@ -628,6 +653,7 @@ class HPCOpsAttnBackend(AttentionBackend):
|
|||||||
mtp=0,
|
mtp=0,
|
||||||
new_kv_included=True,
|
new_kv_included=True,
|
||||||
splitk=True,
|
splitk=True,
|
||||||
|
**task_map_kwargs,
|
||||||
)
|
)
|
||||||
return o.view(-1, layer.tp_q_head_num * layer.head_dim)
|
return o.view(-1, layer.tp_q_head_num * layer.head_dim)
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ reduce into one call and consume *global* top-k expert ids together with
|
|||||||
``rank_ep`` / ``num_expert_total``, so expert parallelism with contiguous
|
``rank_ep`` / ``num_expert_total``, so expert parallelism with contiguous
|
||||||
expert partitioning works without a local-expert remap.
|
expert partitioning works without a local-expert remap.
|
||||||
|
|
||||||
Only supported on NVIDIA Hopper / Blackwell (sm90+). Note that the HPC-Ops
|
Only supported on NVIDIA Hopper (SM90; the kernels ship sm90a). Note that the
|
||||||
kernels are currently tuned primarily for H20: on other GPUs (H100/H200/B200,
|
HPC-Ops kernels are currently tuned primarily for H20: on other SM90 GPUs
|
||||||
...) the speedup over the default MoE runner may be limited or absent. Enable
|
(H100/H200) the speedup over the default MoE runner may be limited or absent.
|
||||||
it explicitly with ``--moe-runner-backend hpc_ops``.
|
Enable it explicitly with ``--moe-runner-backend hpc_ops``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ class MoeRunner:
|
|||||||
elif runner_backend.is_cutlass():
|
elif runner_backend.is_cutlass():
|
||||||
self.runner_core = None # CUTLASS uses the direct cutlass_moe_fp4 path
|
self.runner_core = None # CUTLASS uses the direct cutlass_moe_fp4 path
|
||||||
elif runner_backend.is_hpc_ops():
|
elif runner_backend.is_hpc_ops():
|
||||||
|
import torch
|
||||||
|
|
||||||
|
major, minor = torch.cuda.get_device_capability()
|
||||||
|
if major != 9:
|
||||||
|
raise ValueError(
|
||||||
|
"--moe-runner-backend hpc_ops requires an SM90 (Hopper) "
|
||||||
|
"GPU (the HPC-Ops kernels ship sm90a only), got "
|
||||||
|
f"sm{major}{minor}."
|
||||||
|
)
|
||||||
self.runner_core = None # HPC-Ops only supports the fused path
|
self.runner_core = None # HPC-Ops only supports the fused path
|
||||||
# Import here (not at module top, to avoid a circular import) to
|
# Import here (not at module top, to avoid a circular import) to
|
||||||
# register the hpc_ops fused func before the pool lookup.
|
# register the hpc_ops fused func before the pool lookup.
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ ATTENTION_BACKEND_CHOICES = [
|
|||||||
"tokenspeed_mla",
|
"tokenspeed_mla",
|
||||||
"trtllm_mha",
|
"trtllm_mha",
|
||||||
"dual_chunk_flash_attn",
|
"dual_chunk_flash_attn",
|
||||||
"hpc_ops", # HPC-Ops (https://github.com/Tencent/hpc-ops), Hopper+, requires --page-size 64
|
"hpc_ops", # HPC-Ops (https://github.com/Tencent/hpc-ops), Hopper (SM90) only, requires --page-size 64
|
||||||
# AMD specific
|
# AMD specific
|
||||||
"aiter",
|
"aiter",
|
||||||
"wave",
|
"wave",
|
||||||
@@ -259,7 +259,7 @@ MOE_RUNNER_BACKEND_CHOICES = [
|
|||||||
"marlin",
|
"marlin",
|
||||||
"humming",
|
"humming",
|
||||||
"experimental_sgl_marlin",
|
"experimental_sgl_marlin",
|
||||||
"hpc_ops", # HPC-Ops (https://github.com/Tencent/hpc-ops), FP8 MoE on Hopper+
|
"hpc_ops", # HPC-Ops (https://github.com/Tencent/hpc-ops), FP8 MoE on Hopper (SM90) only
|
||||||
]
|
]
|
||||||
|
|
||||||
MOE_A2A_BACKEND_CHOICES = [
|
MOE_A2A_BACKEND_CHOICES = [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Compares the hpc_ops fused func (hpc.fuse_moe_blockwise) against the triton
|
Compares the hpc_ops fused func (hpc.fuse_moe_blockwise) against the triton
|
||||||
fused_experts reference and an fp32 exact reference on realistic blockwise
|
fused_experts reference and an fp32 exact reference on realistic blockwise
|
||||||
FP8 quantized weights. Skipped when HPC-Ops (https://github.com/Tencent/hpc-ops)
|
FP8 quantized weights. Skipped when HPC-Ops (https://github.com/Tencent/hpc-ops)
|
||||||
is not installed or the GPU is older than sm90.
|
is not installed or the GPU is not SM90 (the kernels ship sm90a only).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -35,11 +35,11 @@ register_cuda_ci(est_time=60, stage="base-b", runner_config="1-gpu-large")
|
|||||||
E, TOPK, H, I = 128, 8, 2048, 768
|
E, TOPK, H, I = 128, 8, 2048, 768
|
||||||
|
|
||||||
|
|
||||||
def _sm90_or_newer() -> bool:
|
def _sm90() -> bool:
|
||||||
if not torch.cuda.is_available():
|
if not torch.cuda.is_available():
|
||||||
return False
|
return False
|
||||||
major, _ = torch.cuda.get_device_capability()
|
major, _ = torch.cuda.get_device_capability()
|
||||||
return major >= 9
|
return major == 9
|
||||||
|
|
||||||
|
|
||||||
def _ensure_dist_initialized() -> None:
|
def _ensure_dist_initialized() -> None:
|
||||||
@@ -76,8 +76,8 @@ def _quant_blockwise(w: torch.Tensor, block: int = 128):
|
|||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
has_hpc_ops() and _sm90_or_newer(),
|
has_hpc_ops() and _sm90(),
|
||||||
"requires HPC-Ops (install from source: https://github.com/Tencent/hpc-ops) and sm90+",
|
"requires HPC-Ops (install from source: https://github.com/Tencent/hpc-ops) and an SM90 (Hopper) GPU",
|
||||||
)
|
)
|
||||||
class TestHpcOpsMoeBlockwise(CustomTestCase):
|
class TestHpcOpsMoeBlockwise(CustomTestCase):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user