whn09 3629d113fb Neuron: prompt chunks through the decode region graphs (issue #26)
- PrepareArgs.prompt_as_decode: decode-layout metadata for a prompt chunk;
  input_ids are the prompt's own tokens instead of last token + MTP drafts.
- DEEPINFER_NEURON_PREFILL_BUCKET adds one graph size; the model runner pads
  a prompt chunk to it with the decode placeholders (every EP rank takes the
  same bucket, so the static collectives match). Sampling still sees a prompt
  step.
- nki_attention.paged_mqa_logits: one gather + one kernel call for a chunk
  (one sequence) instead of a T-way loop.
- bf16 linear for the LM head eagerly on a NeuronCore (b.float() was a
  2.6 GiB copy a call); DEEPINFER_NEURON_REGION_TIMING; --num_prompts.

Host mode, tiny V4.1, chunk 128: same tokens as eager prefill (short 9/9,
long 715 tokens in 6 chunks 8/9 against golden).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:19:56 +08:00

428 lines
16 KiB
Python

"""Dense / grouped GEMMs; mirrors ``DeepGEMM-Ascend/csrc/apis/gemm.hpp``.
Everything is a straightforward dequantize-then-matmul reference with fp32
accumulation. Outputs are written in place into the caller's ``d`` tensor, and
``c`` follows the upstream contract:
* ``c is d`` (same storage) -> accumulate: ``D = A @ B^T + D``
* separate contiguous ``[1, N]`` tensor -> bias
* ``None`` -> plain ``D = A @ B^T``
Operand majorness does not change the logical shapes, so the ``nt`` / ``nn`` /
``tn`` / ``tt`` variants only differ by which operands are logically transposed.
"""
import torch
try: # the NKI kernels live in deepinfer; standalone shim tests run without it
from deepinfer.neuron_ops import nki_gemm
except ImportError: # pragma: no cover
nki_gemm = None
from ._quant import dequantize, is_packed_fp4
from .layout import transform_sf_into_required_layout
def _is_alias(a: torch.Tensor, b: torch.Tensor) -> bool:
return a.data_ptr() == b.data_ptr() and a.shape == b.shape
def _logical_k(data: torch.Tensor) -> int:
"""Logical K of a low-precision operand whose K axis is the last dim."""
return data.shape[-1] * (2 if is_packed_fp4(data.dtype) else 1)
def _write_output(out: torch.Tensor, d: torch.Tensor, c: torch.Tensor | None, alpha: float | None) -> None:
if alpha is not None:
out = out * alpha
if c is not None:
if _is_alias(c, d):
out = out + d.float()
else:
# Separate bias tensor, [1, N] (or broadcastable).
assert c.shape[-1] == d.shape[-1], f"{c.shape=}, {d.shape=}"
out = out + c.float()
d.copy_(out.to(d.dtype))
def _dequant_operand(
data: torch.Tensor,
sf: torch.Tensor,
mn: int,
k: int,
recipe: tuple[int, int, int],
is_sfa: bool,
num_groups: int | None = None,
disable_ue8m0_cast: bool = False,
) -> torch.Tensor:
sf = transform_sf_into_required_layout(
sf, mn, k, recipe, num_groups=num_groups, is_sfa=is_sfa, disable_ue8m0_cast=disable_ue8m0_cast
)
return dequantize(data, sf, gran_k=recipe[2])
# Dequantized weight rows per chunk (fp32 elements). The device is a 24 GB NeuronCore that already holds the
# packed weights: dequantizing a whole projection (or all local experts) to fp32 at once ran the full model out
# of HBM. Chunking along N keeps every output element's K-reduction intact, so results are unchanged.
_CHUNK_ELEMS = 1 << 25
def _row_chunks(n: int, k: int) -> list[tuple[int, int]]:
step = max(1, _CHUNK_ELEMS // max(1, k))
return [(n0, min(n, n0 + step)) for n0 in range(0, n, step)]
def _required_sf(sf, mn, k, recipe, is_sfa, num_groups=None, disable_ue8m0_cast=False):
return transform_sf_into_required_layout(
sf, mn, k, recipe, num_groups=num_groups, is_sfa=is_sfa, disable_ue8m0_cast=disable_ue8m0_cast
)
# --------------------------------------------------------------------------- #
# BF16
# --------------------------------------------------------------------------- #
def _bf16_gemm(
a: torch.Tensor,
b: torch.Tensor,
d: torch.Tensor,
c: torch.Tensor | None,
trans_a: bool,
trans_b: bool,
alpha: float | None,
) -> None:
if trans_a:
a = a.transpose(0, 1)
if trans_b:
b = b.transpose(0, 1)
assert a.dtype == b.dtype == torch.bfloat16, f"{a.dtype=}, {b.dtype=}"
assert d.dtype in (torch.bfloat16, torch.float32), f"{d.dtype=}"
m, k = a.shape
n = b.shape[0]
assert b.shape[1] == k and d.shape == (m, n), f"{a.shape=}, {b.shape=}, {d.shape=}"
if m == 0 or n == 0:
return
if k == 0:
_write_output(torch.zeros((m, n), dtype=torch.float32, device=d.device), d, c, alpha)
return
if torch.compiler.is_compiling() or b.device.type == "neuron":
# NEURON graphs: one bf16 dot with fp32 accumulation and no materialized fp32 transpose (vllm-neuron#71 runs
# these as bf16 linears; an fp32 transpose feeding a dot is a neuronx-cc InferNonlocalTensors trigger).
# Eagerly on a NeuronCore too: ``b.float()`` of the LM head is a 2.6 GiB copy a call.
_write_output(torch.nn.functional.linear(a, b).float(), d, c, alpha)
return
_write_output(a.float() @ b.float().transpose(0, 1), d, c, alpha)
def bf16_gemm_nt(a, b, d, c=None, compiled_dims="nk", alpha=None, epilogue=None) -> None:
"""D = A[M, K] @ B[N, K]^T (+ C)."""
assert epilogue is None, "epilogue classes are not supported"
_bf16_gemm(a, b, d, c, False, False, alpha)
def bf16_gemm_nn(a, b, d, c=None, compiled_dims="nk", alpha=None, epilogue=None) -> None:
"""D = A[M, K] @ B[K, N] (+ C)."""
assert epilogue is None, "epilogue classes are not supported"
_bf16_gemm(a, b, d, c, False, True, alpha)
def bf16_gemm_tn(a, b, d, c=None, compiled_dims="mn", alpha=None, epilogue=None) -> None:
"""D = A[K, M]^T @ B[K, N] (+ C)."""
assert epilogue is None, "epilogue classes are not supported"
_bf16_gemm(a, b, d, c, True, True, alpha)
def bf16_gemm_tt(a, b, d, c=None, compiled_dims="mn", alpha=None, epilogue=None) -> None:
"""D = A[K, M]^T @ B[N, K]^T (+ C)."""
assert epilogue is None, "epilogue classes are not supported"
_bf16_gemm(a, b, d, c, True, False, alpha)
# The Ascend build aliases the cuBLASLt entry points onto its ACLNN bf16 GEMMs
# (``csrc/apis/aclnn.hpp``), so they are the same kernel here.
def cublaslt_gemm_nt(a, b, d, c=None) -> None:
_bf16_gemm(a, b, d, c, False, False, None)
def cublaslt_gemm_nn(a, b, d, c=None) -> None:
_bf16_gemm(a, b, d, c, False, True, None)
def cublaslt_gemm_tn(a, b, d, c=None) -> None:
_bf16_gemm(a, b, d, c, True, True, None)
def cublaslt_gemm_tt(a, b, d, c=None) -> None:
_bf16_gemm(a, b, d, c, True, False, None)
aclnn_bf16_gemm_nt = cublaslt_gemm_nt
aclnn_bf16_gemm_nn = cublaslt_gemm_nn
aclnn_bf16_gemm_tn = cublaslt_gemm_tn
aclnn_bf16_gemm_tt = cublaslt_gemm_tt
# --------------------------------------------------------------------------- #
# FP8 / FP4
# --------------------------------------------------------------------------- #
def _transposed_pair(
operand: tuple[torch.Tensor, torch.Tensor], trans: bool, dim0: int, dim1: int
) -> tuple[torch.Tensor, torch.Tensor]:
data, sf = operand
if not trans:
return data, sf
assert not is_packed_fp4(data.dtype), "packed FP4 operands cannot be logically transposed"
return data.transpose(dim0, dim1), sf.transpose(dim0, dim1)
def _fp8_fp4_gemm(
a_: tuple[torch.Tensor, torch.Tensor],
b_: tuple[torch.Tensor, torch.Tensor],
d: torch.Tensor,
c: torch.Tensor | None,
recipe,
recipe_a,
recipe_b,
disable_ue8m0_cast: bool,
alpha: float | None,
trans_a: bool,
trans_b: bool,
) -> None:
from .layout import get_recipe
a, sfa = _transposed_pair(a_, trans_a, 0, 1)
b, sfb = _transposed_pair(b_, trans_b, 0, 1)
assert d.dtype in (torch.bfloat16, torch.float32), f"{d.dtype=}"
m = a.shape[0]
n = b.shape[0]
k = _logical_k(a)
assert _logical_k(b) == k, f"{a.shape=}, {b.shape=}"
assert d.shape == (m, n), f"{d.shape=}, {m=}, {n=}"
if m == 0 or n == 0:
return
if c is None and alpha is None and not trans_a and not trans_b and nki_gemm is not None and nki_gemm.supported(a, b, k, n):
nki_gemm.gemm(a, sfa, b, d)
return
full_recipe = get_recipe(recipe, recipe_a, recipe_b)
a_f32 = _dequant_operand(a, sfa, m, k, full_recipe, True, disable_ue8m0_cast=disable_ue8m0_cast)
sfb_req = _required_sf(sfb, n, k, full_recipe, False, disable_ue8m0_cast=disable_ue8m0_cast)
c_is_d = c is not None and _is_alias(c, d)
for n0, n1 in _row_chunks(n, k):
b_f32 = dequantize(b[n0:n1], sfb_req[n0:n1], gran_k=full_recipe[2])
c_chunk = None if c is None else (d[:, n0:n1] if c_is_d else c[..., n0:n1])
_write_output(a_f32 @ b_f32.transpose(0, 1), d[:, n0:n1], c_chunk, alpha)
def fp8_fp4_gemm_nt(
a, b, d, c=None, recipe=None, recipe_a=None, recipe_b=None,
compiled_dims="nk", disable_ue8m0_cast=False, alpha=None, epilogue=None,
) -> None:
"""D = A[M, K] @ B[N, K]^T (+ C), with FP8/FP4 operands and UE8M0 scales."""
assert epilogue is None, "epilogue classes are not supported"
_fp8_fp4_gemm(a, b, d, c, recipe, recipe_a, recipe_b, disable_ue8m0_cast, alpha, False, False)
def fp8_fp4_gemm_nn(
a, b, d, c=None, recipe=None, recipe_a=None, recipe_b=None,
compiled_dims="nk", disable_ue8m0_cast=False, alpha=None, epilogue=None,
) -> None:
assert epilogue is None, "epilogue classes are not supported"
_fp8_fp4_gemm(a, b, d, c, recipe, recipe_a, recipe_b, disable_ue8m0_cast, alpha, False, True)
def fp8_fp4_gemm_tn(
a, b, d, c=None, recipe=None, recipe_a=None, recipe_b=None,
compiled_dims="mn", disable_ue8m0_cast=False, alpha=None, epilogue=None,
) -> None:
assert epilogue is None, "epilogue classes are not supported"
_fp8_fp4_gemm(a, b, d, c, recipe, recipe_a, recipe_b, disable_ue8m0_cast, alpha, True, True)
def fp8_fp4_gemm_tt(
a, b, d, c=None, recipe=None, recipe_a=None, recipe_b=None,
compiled_dims="mn", disable_ue8m0_cast=False, alpha=None, epilogue=None,
) -> None:
assert epilogue is None, "epilogue classes are not supported"
_fp8_fp4_gemm(a, b, d, c, recipe, recipe_a, recipe_b, disable_ue8m0_cast, alpha, True, False)
# Upstream keeps separate FP8-only entry points with the same semantics.
fp8_gemm_nt = fp8_fp4_gemm_nt
fp8_gemm_nn = fp8_fp4_gemm_nn
fp8_gemm_tn = fp8_fp4_gemm_tn
fp8_gemm_tt = fp8_fp4_gemm_tt
aclnn_fp8_fp4_gemm_nt = fp8_fp4_gemm_nt
aclnn_fp8_fp4_gemm_nn = fp8_fp4_gemm_nn
aclnn_fp8_fp4_gemm_tn = fp8_fp4_gemm_tn
aclnn_fp8_fp4_gemm_tt = fp8_fp4_gemm_tt
# --------------------------------------------------------------------------- #
# M-grouped contiguous ("psum") layout
# --------------------------------------------------------------------------- #
def _psum_group_spans(grouped_layout: torch.Tensor, alignment: int, total_m: int) -> list[tuple[int, int]]:
"""Decode the contiguous psum layout into per-group ``[start, end)`` row spans.
``grouped_layout`` is the cumulative end offset of each group's valid rows; the
next group starts at ``align(end, alignment)``
(``DeepGEMM-Ascend/tests/generators.py::generate_psum_layout``).
"""
assert grouped_layout.dim() == 1 and grouped_layout.dtype == torch.int32, f"{grouped_layout.shape=}, {grouped_layout.dtype=}"
# NEURON: host sync - the group boundaries are data dependent, and the number of
# groups is small (one per local expert), so the loop runs on the host.
ends = grouped_layout.tolist()
spans = []
start = 0
for end in ends:
end = max(start, min(int(end), total_m))
spans.append((start, end))
start = min((int(end) + alignment - 1) // alignment * alignment, total_m)
return spans
def m_grouped_bf16_gemm_nt_contiguous(
a: torch.Tensor,
b: torch.Tensor,
d: torch.Tensor,
grouped_layout: torch.Tensor,
compiled_dims="nk",
use_psum_layout: bool = True,
ensure_zero_padding: bool = True,
expected_m_for_psum_layout: int | None = None,
epilogue=None,
) -> None:
"""D[rows of group i] = A[rows of group i] @ B[i]^T, contiguous psum layout."""
assert epilogue is None, "epilogue classes are not supported"
assert use_psum_layout, "only the psum layout is supported"
assert a.dtype == b.dtype == torch.bfloat16, f"{a.dtype=}, {b.dtype=}"
num_groups, n, k = b.shape
total_m = a.shape[0]
assert a.shape[1] == k and d.shape == (total_m, n), f"{a.shape=}, {b.shape=}, {d.shape=}"
if total_m == 0 or n == 0:
return
from .runtime import get_mk_alignment_for_contiguous_layout
spans = _psum_group_spans(grouped_layout, get_mk_alignment_for_contiguous_layout(), total_m)
assert len(spans) == num_groups, f"{len(spans)=}, {num_groups=}"
d.zero_()
for group_idx, (start, end) in enumerate(spans):
if end <= start:
continue
out = a[start:end].float() @ b[group_idx].float().transpose(0, 1)
d[start:end].copy_(out.to(d.dtype))
def m_grouped_bf16_gemm_nn_contiguous(
a, b, d, grouped_layout, compiled_dims="nk", use_psum_layout=True,
ensure_zero_padding=True, expected_m_for_psum_layout=None, epilogue=None,
) -> None:
m_grouped_bf16_gemm_nt_contiguous(
a, b.transpose(1, 2), d, grouped_layout, compiled_dims, use_psum_layout,
ensure_zero_padding, expected_m_for_psum_layout, epilogue,
)
def m_grouped_fp8_fp4_gemm_nt_contiguous(
a_: tuple[torch.Tensor, torch.Tensor],
b_: tuple[torch.Tensor, torch.Tensor],
d: torch.Tensor,
grouped_layout: torch.Tensor,
recipe=None,
recipe_a=None,
recipe_b=None,
compiled_dims="nk",
disable_ue8m0_cast: bool = False,
use_psum_layout: bool = True,
ensure_zero_padding: bool = True,
expected_m_for_psum_layout: int | None = None,
epilogue=None,
) -> None:
"""D[rows of group i] = A[rows of group i] @ B[i]^T for FP8/FP4 operands."""
from .layout import get_recipe
from .runtime import get_mk_alignment_for_contiguous_layout
assert epilogue is None, "epilogue classes are not supported"
assert use_psum_layout, "only the psum layout is supported"
a, sfa = a_
b, sfb = b_
total_m = a.shape[0]
num_groups, n = b.shape[0], b.shape[1]
k = _logical_k(a)
assert _logical_k(b) == k, f"{a.shape=}, {b.shape=}"
assert d.shape == (total_m, n), f"{d.shape=}, {total_m=}, {n=}"
if total_m == 0 or n == 0:
return
full_recipe = get_recipe(recipe, recipe_a, recipe_b)
a_f32 = _dequant_operand(a, sfa, total_m, k, full_recipe, True, disable_ue8m0_cast=disable_ue8m0_cast)
sfb_req = _required_sf(sfb, n, k, full_recipe, False, num_groups=num_groups, disable_ue8m0_cast=disable_ue8m0_cast)
spans = _psum_group_spans(grouped_layout, get_mk_alignment_for_contiguous_layout(), total_m)
assert len(spans) == num_groups, f"{len(spans)=}, {num_groups=}"
d.zero_()
for group_idx, (start, end) in enumerate(spans):
if end <= start:
continue
# One expert (and one N chunk) at a time: never all local experts dequantized at once.
for n0, n1 in _row_chunks(n, k):
b_f32 = dequantize(b[group_idx, n0:n1], sfb_req[group_idx, n0:n1], gran_k=full_recipe[2])
out = a_f32[start:end] @ b_f32.transpose(0, 1)
d[start:end, n0:n1].copy_(out.to(d.dtype))
def m_grouped_fp8_fp4_gemm_nt_shared_a(
a_: tuple[torch.Tensor, torch.Tensor],
b_: tuple[torch.Tensor, torch.Tensor],
d: torch.Tensor,
group_idx: int,
recipe=None,
recipe_a=None,
recipe_b=None,
disable_ue8m0_cast: bool = False,
) -> None:
"""NEURON extension, static shapes: ``D = A @ B[group_idx]^T`` with every row of A (no psum layout, no host
sync). The static decode MoE runs each local expert over all gathered rows."""
from .layout import get_recipe
a, sfa = a_
b, sfb = b_
total_m = a.shape[0]
num_groups, n = b.shape[0], b.shape[1]
k = _logical_k(a)
assert _logical_k(b) == k, f"{a.shape=}, {b.shape=}"
assert d.shape == (total_m, n), f"{d.shape=}, {total_m=}, {n=}"
if nki_gemm is not None and nki_gemm.supported(a, b, k, n):
nki_gemm.gemm(a, sfa, b, d, group=group_idx)
return
full_recipe = get_recipe(recipe, recipe_a, recipe_b)
a_f32 = _dequant_operand(a, sfa, total_m, k, full_recipe, True, disable_ue8m0_cast=disable_ue8m0_cast)
sfb_req = _required_sf(sfb, n, k, full_recipe, False, num_groups=num_groups, disable_ue8m0_cast=disable_ue8m0_cast)
for n0, n1 in _row_chunks(n, k):
b_f32 = dequantize(b[group_idx, n0:n1], sfb_req[group_idx, n0:n1], gran_k=full_recipe[2])
d[:, n0:n1].copy_((a_f32 @ b_f32.transpose(0, 1)).to(d.dtype))
def m_grouped_fp8_fp4_gemm_nn_contiguous(
a, b, d, grouped_layout, recipe=None, recipe_a=None, recipe_b=None,
compiled_dims="nk", disable_ue8m0_cast=False, use_psum_layout=True,
ensure_zero_padding=True, expected_m_for_psum_layout=None, epilogue=None,
) -> None:
data, sf = b
assert not is_packed_fp4(data.dtype), "packed FP4 B cannot be logically transposed"
m_grouped_fp8_fp4_gemm_nt_contiguous(
a, (data.transpose(1, 2), sf.transpose(1, 2)), d, grouped_layout, recipe, recipe_a, recipe_b,
compiled_dims, disable_ue8m0_cast, use_psum_layout, ensure_zero_padding,
expected_m_for_psum_layout, epilogue,
)
m_grouped_fp8_gemm_nt_contiguous = m_grouped_fp8_fp4_gemm_nt_contiguous
m_grouped_fp8_gemm_nn_contiguous = m_grouped_fp8_fp4_gemm_nn_contiguous
__all__ = [name for name in globals() if "gemm" in name]