whn09 c826e81b71 neuron: mHC collapse through torch by default (DEEPINFER_NEURON_HC_TORCH=collapse); fix Neuron bind_cpu core index
collapse in torch is bit-exact with the hc_collapse kernel and a faster graph (512 rows: 0.15 vs 1.73 ms
standalone). post_torch (the kernel's operation order) is bit-equal standalone but rounds differently fused
in the layer graph, so it is opt-in. Tiny golden 9/9 with collapse. bind_cpu receives the physical core id;
_neuron_bind_cpus indexed CUDA_VISIBLE_DEVICES with it again (broke runs on cores other than 0..n).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:18:56 +08:00

179 lines
8.2 KiB
Python

"""mHC (hyper-connection) ops through the vllm-neuron#71 NKI kernels (issue #13).
DeepInfer op (v4_modules/mhc_ops.py, hc.py) kernel (nki/)
expand_to_mhc hc_expand.kernel
mhc_pre_apply_mix (hc_pre collapse) hc_collapse.kernel
mhc_post hc_attn_out.kernel
mhc_pre_norm_fn + split_mixes + sinkhorn (shifted) fp32 fn GEMM (torch) + hc_mixes.kernel
mhc_pre_big_fuse (non-shifted) the same, then hc_collapse.kernel
DeepInfer's semantics are kept: the ``fn`` GEMM stays fp32 (``x.float() @ fn.T``, as the ``tf32_hc_prenorm_gemm``
shim; #71 narrows ``fn`` to bf16), the rms statistic runs over the whole ``hc * hidden`` row (``hc_norm_group_size``).
Inside a compiled graph neuronx-cc runs an fp32 matmul on bf16-narrowed operands (``--auto-cast=none``
notwithstanding: the compiled shim's error equals that of ``fn.bfloat16()``, ~2e-3 of the output range), and it
folds an in-graph ``hi + lo`` split away again. So ``prepare_model`` splits every ``fn`` once, eagerly, into bf16
``hi`` + ``lo`` (``fn - hi``) and the graph runs two exact-operand matmuls with fp32 accumulation (error ~2e-6).
``hc_mixes`` bakes ``hc_sinkhorn_iters`` / ``hc_eps`` / ``rms_norm_eps`` (20 / 1e-6 / 1e-20 in V4.1), the
``2 *`` post multiplier and ``pre = sigmoid + hc_eps``; ``mixes_supported`` checks a layer's values against them
and anything else keeps the shim path. ``hc_expand`` bakes ``hc_mult = 4``.
Activations are bf16 and coefficients fp32 throughout, as in the shims. ``DEEPINFER_NEURON_NKI_HC=0`` restores the
shims.
"""
from __future__ import annotations
import os
import torch
ENABLED = os.environ.get("DEEPINFER_NEURON_NKI_HC", "1") == "1"
def active(t: torch.Tensor, op: str | None = None) -> bool:
return ENABLED and t.device.type == "neuron" and op not in _TORCH_OPS
#: DEEPINFER_NEURON_HC_TORCH: mHC ops that take torch even with the NKI kernels enabled. collapse (the default) is
#: bit-exact and compiles to a faster graph than its kernel. post is opt-in: post_torch is bit-equal to the kernel
#: standalone, but fused into the layer graph it rounds differently (tiny golden 5/9 on the long prompt). The mixes
#: keep the kernel path (the shim's fp32 fn GEMM runs on bf16-narrowed operands in a graph, see above).
_TORCH_OPS = set(filter(None, os.environ.get("DEEPINFER_NEURON_HC_TORCH", "collapse").split(",")))
_KERNELS: dict = {}
def _kernel(module: str): # noqa: ANN202
"""``wrap_nki(nki.<module>.kernel)``, built once (a dict, not functools.cache, which dynamo warns about)."""
k = _KERNELS.get(module)
if k is None:
from torch_neuronx.nki_hop import wrap_nki
from deepinfer.neuron_ops.nki import hc_attn_out, hc_collapse, hc_expand, hc_mixes
mods = {"hc_attn_out": hc_attn_out, "hc_collapse": hc_collapse, "hc_expand": hc_expand, "hc_mixes": hc_mixes}
k = _KERNELS[module] = wrap_nki(mods[module].kernel)
return k
def expand_supported(hidden: torch.Tensor, hc_mult: int) -> bool:
from deepinfer.neuron_ops.nki import hc_expand
return active(hidden) and hc_mult == hc_expand.HC
def expand(hidden: torch.Tensor, hc_mult: int) -> torch.Tensor:
"""``[..., h]`` -> ``[..., hc_mult, h]`` copies (``expand_to_mhc``)."""
h = hidden.shape[-1]
copies, _ = _kernel("hc_expand")(hidden.reshape(1, -1, h))
return copies.view(*hidden.shape[:-1], hc_mult, h)
def pre_apply_mix(x: torch.Tensor, mix: torch.Tensor) -> torch.Tensor:
"""``sum_m mix[..., m] * x[..., m, :]`` in fp32, bf16 out: ``x [..., hc, h]`` bf16, ``mix [..., hc, 1]`` (or
``[..., hc]``) fp32 -> ``[..., h]``."""
hc, h = x.shape[-2:]
y = _kernel("hc_collapse")(x.reshape(1, -1, hc, h), mix.reshape(1, -1, hc).to(torch.float32))
return y.view(*x.shape[:-2], h)
def post(x: torch.Tensor, residual: torch.Tensor, post_layer_mix: torch.Tensor, comb_res_mix: torch.Tensor) -> torch.Tensor:
"""``mhc_post``: ``x [b, n, h]``, ``residual [b, n, hc, h]``, ``post [b, n, hc, 1]``, ``comb [b, n, hc, hc]`` ->
``out[..., j, :] = post[j] * x + sum_i comb[i, j] * residual[i]`` ``[b, n, hc, h]``."""
b, n, hc, h = residual.shape
return _kernel("hc_attn_out")(
x.reshape(b, n, h), residual, post_layer_mix.reshape(b, n, hc), comb_res_mix.reshape(b, n, hc, hc)
)
def post_torch(x: torch.Tensor, residual: torch.Tensor, post_layer_mix: torch.Tensor, comb_res_mix: torch.Tensor) -> torch.Tensor:
"""``post`` in torch, in ``hc_attn_out``'s fp32 operation order (``comb`` terms first, ``post * x`` last), so it
compiles to plain elementwise work instead of the kernel (DEEPINFER_NEURON_HC_TORCH=post)."""
b, n, hc, h = residual.shape
res = residual.float()
xf = x.reshape(b, n, 1, h).float()
comb = comb_res_mix.reshape(b, n, hc, hc)
acc = res[:, :, 0:1, :] * comb[:, :, 0, :].unsqueeze(-1) # [b, n, j, h]: comb[0, j] * res[0]
for i in range(1, hc):
acc = res[:, :, i : i + 1, :] * comb[:, :, i, :].unsqueeze(-1) + acc
out = xf * post_layer_mix.reshape(b, n, hc, 1) + acc
return out.to(residual.dtype)
def split_fn(fn: torch.Tensor) -> None:
"""Attach ``fn``'s bf16 ``hi`` / ``lo`` halves (``hi + lo == fn`` to ~2**-17); eager only, see module doc."""
hi = fn.detach().to(torch.bfloat16)
fn._nki_fn_hi = hi
fn._nki_fn_lo = (fn.detach() - hi.float()).to(torch.bfloat16)
def prepare_model(model: torch.nn.Module) -> int:
"""``split_fn`` every mHC ``fn`` of the model (``hc_attn_fn`` / ``hc_ffn_fn``). Returns the count."""
if not ENABLED:
return 0
count = 0
for name, p in model.named_parameters():
if name.endswith(("hc_attn_fn", "hc_ffn_fn")) and p.dtype == torch.float32 and p.dim() == 2:
split_fn(p)
count += 1
return count
def _fn_gemm(flat: torch.Tensor, fn: torch.Tensor) -> torch.Tensor:
"""``flat.float() @ fn.T`` at fp32 accuracy, also inside a compiled graph."""
if getattr(fn, "_nki_fn_hi", None) is None and not torch.compiler.is_compiling():
split_fn(fn)
hi = getattr(fn, "_nki_fn_hi", None)
xf = flat.float()
if hi is None: # not prepared and already tracing: bf16-narrowed fn in a graph, like the shim
return xf @ fn.transpose(0, 1)
return xf @ hi.float().transpose(0, 1) + xf @ fn._nki_fn_lo.float().transpose(0, 1)
def mixes_supported(
residual: torch.Tensor,
norm_eps: float,
pre_eps: float,
sinkhorn_eps: float,
post_mult_value: float,
sinkhorn_repeat: int,
) -> bool:
from deepinfer.neuron_ops.nki import hc_mixes
return (
active(residual)
and residual.dtype == torch.bfloat16
and sinkhorn_repeat == hc_mixes.SINKHORN_ITERS
and pre_eps == hc_mixes.HC_EPS
and sinkhorn_eps == hc_mixes.HC_EPS
and norm_eps == hc_mixes.NORM_EPS
and post_mult_value == 2.0
)
def mixes(
residual: torch.Tensor, fn: torch.Tensor, scale: torch.Tensor, base: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""The layer's three mixes from the trunk ``[..., hc, h]`` bf16: ``pre [..., hc, 1]``, ``post [..., hc, 1]``
and the Sinkhorn-normalized ``comb [..., hc, hc]``, all fp32 (``mhc_pre_norm_fn`` + ``mhc_pre_split_mixes`` +
``sinkhorn_normalize``). ``fn [(2 + hc) * hc, hc * h]`` fp32 already carries the norm weight."""
hc, h = residual.shape[-2:]
lead = residual.shape[:-2]
flat = residual.reshape(-1, hc * h)
raw = _fn_gemm(flat, fn) # fp32, as tf32_hc_prenorm_gemm's shim
pre, post_mix, comb = _kernel("hc_mixes")(raw, flat, base.reshape(1, -1), scale.reshape(1, 3))
return pre.view(*lead, hc, 1), post_mix.view(*lead, hc, 1), comb.view(*lead, hc, hc)
def pre_big_fuse(
residual: torch.Tensor, fn: torch.Tensor, scale: torch.Tensor, base: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""``mhc_pre_big_fuse``: ``(post [..., hc, 1], comb [..., hc, hc], layer_input [..., h])``."""
pre, post_mix, comb = mixes(residual, fn, scale, base)
if "collapse" in _TORCH_OPS:
from deepinfer.model_executor.models.v4_modules.mhc_ops import mhc_pre_apply_mix
return post_mix, comb, mhc_pre_apply_mix(residual, pre)
return post_mix, comb, pre_apply_mix(residual, pre)