mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 08:46:21 +08:00
DeepInfer targets Blackwell: the vendored FlashMLA fork only ships SM100 kernels (`CUDA_ARCHS = ["sm_100a", "sm_103a"]`, every entry point asserts `arch.is_sm100f()`), and several DeepGEMM attention APIs plus the whole quantization recipe assume FP4 tensor cores, packed UE8M0 scales and per-32 blocks. None of that exists on Hopper, so this branch adds an SM90 backend and gets an 8x H20 node to produce tokens end to end. Scope and status: the pipeline runs (prefill, decode, sampling) and matches the reference on the first token, but numerics still diverge from position ~12 onward, so the output is not yet correct. H20-DEBUG-PLAN.md tracks the remaining work with the exact reproduction case; see H20-SM90-FINDINGS.md for the analysis. Quantization - `Platform.sf_pack_factor` separates two meanings that `fp8_sf_type` conflated: the GEMM scale type (FP32 on SM90) and the number of scales packed per element (1, instead of 4 UE8M0 exponents per int32). KV caches keep the ue8m0 byte layout. - Requantize FP4 experts and ue8m0 dense weights to FP8 E4M3 with FP32 per-128 scales while loading (`hugging_face/weights.py`, `h20_ops/quant.py`), so the runtime sees a plain FP8 checkpoint: `mxfp4` is disabled at runtime, expert weights are repacked as 2D, and `use_fp4_sparse_kv` is off since the FP4 cache kernels do not exist on Hopper. - GEMM recipes switch to `(1, 128, 128)` with `disable_ue8m0_cast=True`, which is the only shape SM90 DeepGEMM accepts. Attention - `h20_ops/sparse_mla_sm90.py` adapts TileLang's `examples/deepseek_v32` sparse MLA kernel (which is Hopper-targeted) to the V4.1 layout: RoPE lives inside the 512-wide latent, so the V3.2 `dim`/`tail` split is removed and QK/PV are both 512. Measured on H20: 104 TFLOP/s, bf16-noise accuracy. It takes no causal mask, because the indices are already causal -- deriving causality from the row number drops every index once the KV buffer is offset, which produced an all-NaN layer 0. - `h20_ops/attn_sm90.py` re-implements `prefill_attn_varlen_sparse` and `decode_mla_mqa_sparse` with the same signatures as `attention_backend.py`, and registers them through `patch_imports`, so call sites are untouched and the FlashMLA-only module is never imported. - Softmax normalisation clamps `sumexp`, so a padded row with no valid index yields zeros instead of 0/0 and cannot poison the whole hidden state. Engram - `h20_ops/engram_local.py` replaces `deep_ep.EngramBuffer.fetch`, which needs NCCL GIN (RDMA) and is unavailable on a single node, with a distributed gather over NVLink built from two `all_to_all`s. Row ownership follows the C++ layout (`owner = idx // num_local`). Verified against a full reference table on 8 ranks. Other Blackwell assumptions removed - DeepSelect's top-k kernel faults at launch on SM90, so `h20_ops/deep_select_sm90.py` reproduces its contract with `torch.topk` (end truncation, OOB fill, in-place output views). The shared-layer candidate-block optimisation is disabled because `fp8_fp4_sparse_mqa_logits` has no SM90 implementation, and the dense MQA-logits path is used instead; its fp32 output is narrowed for the block-max kernel. - Decode-time index scoring uses `_decode_p_sum_sm90`: Hopper's paged kernel hard-requires 64-token pages while the cache uses 128, and the pages are strided views that cannot be split without a copy. - `mega_mhc` / `mega_moe`, the FlashAttention-4 CuTe DSL kernels and the fused norm+rope+cast kernel are all SM100-only and take a Hopper fallback. - `allow_cuda_sync()` is used for the few places that genuinely need a host read (all-to-all sizing, the ViT fallback, the NaN diagnostic) instead of disabling the sync guard globally. Vendored submodules (not part of this commit; submodule gitlinks are unchanged) - `vendor/DeepSelect/setup.py`: re-enable the `sm_90a` gencode that is commented out, otherwise top-k fails with "no kernel image". Scripts: `h20_attn_check.py`, `h20_engram_check.py`, `h20_topk_check.py`, `h20_smoke_check.py`, `h20_real_requant_check.py` are unit checks that run on 8x H20; `h20_ask.py` is a one-prompt harness and `ref_next_token.py` produces the official reference next-token for arbitrary ids, so both sides can be compared on identical input.
196 lines
7.3 KiB
Python
196 lines
7.3 KiB
Python
from typing import Any
|
|
|
|
import deep_gemm
|
|
import tile_kernels
|
|
import torch
|
|
|
|
from deepinfer import PLATFORM
|
|
from deepinfer.model_executor.utils import reconstruct_param
|
|
|
|
_OUTPUT_ALIGNMENT_BYTES = 32
|
|
|
|
|
|
def get_fp8_casting_granularity() -> int:
|
|
"""FP8 GEMM quantization block. SM100 uses MXFP8 per-32; Hopper uses per-128."""
|
|
return 128 if PLATFORM.is_sm90 else 32
|
|
|
|
|
|
def get_gemm_recipe() -> tuple[int, int, int]:
|
|
"""GEMM SF recipe. SM90 only has 1D2D (per-token A, 128x128 B); SM100 uses 1D1D per-32."""
|
|
gran = get_fp8_casting_granularity()
|
|
return (1, gran, gran) if PLATFORM.is_sm90 else (1, 1, gran)
|
|
|
|
|
|
def get_fp8_cast_params() -> dict[str, Any]:
|
|
return {
|
|
"fmt": "e4m3",
|
|
"num_per_channels": get_fp8_casting_granularity(),
|
|
"round_sf": True,
|
|
# SM100 packs four UE8M0 exponents into one int32; SM90 wants plain FP32 scales.
|
|
"use_packed_ue8m0": not PLATFORM.is_sm90,
|
|
"use_tma_aligned_col_major_sf": True,
|
|
}
|
|
|
|
|
|
def bf16_gemm_nt(
|
|
a: torch.Tensor,
|
|
b: torch.Tensor,
|
|
d: torch.Tensor,
|
|
c: torch.Tensor | None = None,
|
|
) -> None:
|
|
deep_gemm.bf16_gemm_nt(a, b, d, c)
|
|
|
|
|
|
class ReplicatedLinear(torch.nn.Module):
|
|
"""Replicated linear layer.
|
|
|
|
Args:
|
|
input_size: input dimension of the linear layer.
|
|
output_size: output dimension of the linear layer.
|
|
use_fp32_output: If true, output fp32 instead of input dtype (bf16 weight only).
|
|
weight: weight parameter of shape (output_size, input_size).
|
|
bias: optional bias parameter.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
input_size: int,
|
|
output_size: int,
|
|
use_fp32_output: bool = False,
|
|
*,
|
|
weight: torch.nn.Parameter,
|
|
bias: torch.nn.Parameter | None = None,
|
|
) -> None:
|
|
super().__init__()
|
|
|
|
# Keep input parameters
|
|
self.input_size = input_size
|
|
self.output_size = output_size
|
|
if weight.dtype != torch.bfloat16:
|
|
assert not use_fp32_output
|
|
self.use_fp32_output = use_fp32_output
|
|
|
|
assert weight.shape == (output_size, input_size)
|
|
self.weight = reconstruct_param(weight)
|
|
if self.weight.element_size() == 1:
|
|
self.weight.sf = weight.sf
|
|
if self.weight.sf.dtype != PLATFORM.fp8_sf_type or PLATFORM.is_sm90:
|
|
# SM90 keeps FP32 scales but still needs DeepGEMM's TMA-aligned layout.
|
|
self.weight.sf = deep_gemm.transform_sf_into_required_layout(
|
|
self.weight.sf,
|
|
self.weight.size(0),
|
|
self.weight.size(1),
|
|
recipe=(1, get_fp8_casting_granularity(), get_fp8_casting_granularity()),
|
|
is_sfa=False,
|
|
disable_ue8m0_cast=PLATFORM.is_sm90,
|
|
)
|
|
|
|
if bias is None:
|
|
self.register_parameter("bias", None)
|
|
else:
|
|
self.bias = bias
|
|
|
|
def _allocate_output(
|
|
self,
|
|
shape: tuple[int, ...],
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
) -> torch.Tensor:
|
|
assert len(shape) >= 2 and shape[-1] == self.output_size, f"{shape=}, {self.output_size=}"
|
|
itemsize = dtype.itemsize
|
|
assert _OUTPUT_ALIGNMENT_BYTES % itemsize == 0, f"{dtype=}"
|
|
outputs_per_alignment = _OUTPUT_ALIGNMENT_BYTES // itemsize
|
|
physical_output_size = (self.output_size + outputs_per_alignment - 1) // outputs_per_alignment * outputs_per_alignment
|
|
storage = torch.empty((*shape[:-1], physical_output_size), dtype=dtype, device=device)
|
|
return storage[..., : self.output_size]
|
|
|
|
def _has_aligned_output_stride(self, output: torch.Tensor) -> bool:
|
|
assert output.ndim >= 2 and output.shape[-1] == self.output_size and output.stride(-1) == 1
|
|
return output.stride(-2) * output.element_size() % _OUTPUT_ALIGNMENT_BYTES == 0
|
|
|
|
def _flatten_output(self, output: torch.Tensor) -> torch.Tensor:
|
|
assert output.ndim >= 2 and output.shape[-1] == self.output_size and output.stride(-1) == 1
|
|
assert all(output.stride(i) == output.stride(i + 1) * output.shape[i + 1] for i in range(output.ndim - 2)), (
|
|
f"{output.shape=}, {output.stride()=}"
|
|
)
|
|
m = output.shape[:-1].numel()
|
|
return output.as_strided((m, self.output_size), (output.stride(-2), 1))
|
|
|
|
def _align_output(self, output: torch.Tensor) -> torch.Tensor:
|
|
if self._has_aligned_output_stride(output):
|
|
return output
|
|
aligned = self._allocate_output(tuple(output.shape), output.dtype, output.device)
|
|
aligned.copy_(output)
|
|
return aligned
|
|
|
|
def apply_weight_bf16(
|
|
self,
|
|
x: torch.Tensor,
|
|
bias: torch.Tensor | None = None,
|
|
out_acc: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
weight = self.weight
|
|
m_shape = x.shape[:-1]
|
|
m = m_shape.numel()
|
|
n, k = weight.shape
|
|
if out_acc is None:
|
|
out = self._allocate_output((*m_shape, n), torch.float if self.use_fp32_output else x.dtype, x.device)
|
|
else:
|
|
out = out_acc
|
|
assert out.shape == (*m_shape, n)
|
|
assert out.dtype == (torch.float if self.use_fp32_output else x.dtype)
|
|
assert out.device == x.device
|
|
out_acc = self._flatten_output(out_acc)
|
|
assert self._has_aligned_output_stride(out), f"unaligned output: {out.shape=}, {out.stride()=}, {out.dtype=}"
|
|
bf16_gemm_nt(x.view(m, k), weight, self._flatten_output(out), out_acc)
|
|
if bias is not None:
|
|
out += bias
|
|
return out
|
|
|
|
def apply_weight_fp8(
|
|
self,
|
|
x: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
|
|
bias: torch.Tensor | None = None,
|
|
out_acc: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
assert bias is None
|
|
|
|
if isinstance(x, torch.Tensor):
|
|
assert x.dim() == 2, f"{x.shape=}, {x.stride()=}"
|
|
x = tile_kernels.quant.per_token_cast(x, **get_fp8_cast_params())
|
|
|
|
# FP8 GEMM.
|
|
weight = (self.weight, self.weight.sf)
|
|
out_shape = x[0].shape[:-1] + (self.weight.shape[0],)
|
|
if out_acc is None:
|
|
out = self._allocate_output(out_shape, torch.bfloat16, x[0].device)
|
|
else:
|
|
out = out_acc
|
|
assert out.shape == out_shape
|
|
assert out.dtype == torch.bfloat16
|
|
assert out.device == x[0].device
|
|
|
|
assert self._has_aligned_output_stride(out), f"unaligned output: {out.shape=}, {out.stride()=}, {out.dtype=}"
|
|
assert x[1].dtype == weight[1].dtype == PLATFORM.fp8_sf_type, f"{x[1].dtype=}, {weight[1].dtype=}"
|
|
deep_gemm.fp8_fp4_gemm_nt(x, weight, out, out_acc, recipe=get_gemm_recipe(), disable_ue8m0_cast=PLATFORM.is_sm90)
|
|
return out
|
|
|
|
def apply_weight(
|
|
self,
|
|
x: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
|
|
bias: torch.Tensor | None,
|
|
out_acc: torch.Tensor | None,
|
|
) -> torch.Tensor:
|
|
if self.weight.dtype == torch.bfloat16:
|
|
return self.apply_weight_bf16(x, bias, out_acc)
|
|
if self.weight.element_size() == 1:
|
|
return self.apply_weight_fp8(x, bias, out_acc)
|
|
raise TypeError(f"unsupported weight dtype: {self.weight.dtype}")
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
|
|
out_acc: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
return self.apply_weight(x, self.bias, out_acc)
|