whn09 9c8c85d83d Neuron: per-layer region graphs + vllm-neuron#71 NKI kernels (issues #10, #11, #13, #25)
- Decode runs eagerly and compiles per-layer regions (attention half, MoE) with stage boundaries
  (neuron_ops/regions.py, DEEPINFER_NEURON_GRAPH_MODE=regions); layer KV views are sliced from the
  roots inside the graph (_LazyKVCache).
- vllm-neuron#71 NKI kernels (Apache-2.0, vendored under neuron_ops/nki/ with attribution), called via
  torch_neuronx.nki_hop.wrap_nki: blockquant (fp8/fp4 GEMMs incl. grouped einsum, MoE and shared
  expert on bf16 activations), sparse_attn / index_score / compress_pool (nki_attention.py),
  hc_mixes / hc_expand / hc_collapse / hc_attn_out / rmsnorm / swiglu (nki_hc.py, nki_norm.py).
- Compile-safe formulations: row-major scales, no dtype bitcasts of graph inputs (uint8 weight
  tensors), whole-row KV stores, byte assembly in int32 narrowed once at the store, arithmetic int16
  scale packing, plain fma/rsqrt and bf16 F.linear under compile; default backend deepinfer_neuron
  (torch fp8 carried as uint8, avoiding NCC_EOCP001 with the kernels' OCP fp8).
- Host: tiny model 9/9 vs golden on all prompts; device: kernel tests pass (13 attention, 88 hc/norm);
  region decode graph compiles except the attention projection stage (being reduced).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:36:54 +08:00

174 lines
8.1 KiB
Python

"""Mirrors ``tilelang_ops/asc/compress.py``
(compress_and_update_state_prefill_tl / compress_and_update_state_decode_tl).
Softmax-weighted compression of ``COMPRESS_RATIO`` (1 or 2) consecutive (score, latent) fp32 rows into one
bf16 row, plus the rolling ``state_cache[slot, pos % state_cache_size, {score, latent}, :]`` update.
Numerics follow the Ascend kernel: ``m = max_r score``, ``e_r = exp(score_r - m)``, ``sum = sum_r e_r``,
``acc = sum_r latent_r * e_r`` (fp32, row order), ``out = bf16(acc * (1 / sum))``.
Order: prefill reads the previous chunk's tail from ``state_cache`` *before* storing the new tokens;
decode stores every token first and then reads the compression windows (as the two Ascend sub-kernels).
All index math is static-shape tensor ops (no host syncs).
"""
from collections.abc import Callable
import torch
def _compress_rows(score_rows: torch.Tensor, latent_rows: torch.Tensor) -> torch.Tensor:
"""score_rows / latent_rows: [N, R, D] fp32 -> [N, D] bf16 (Ascend operation order)."""
from deepinfer.neuron_ops import nki_attention
if nki_attention.supported_pool(score_rows): # vllm-neuron#71's softmax_pool kernel (issue #11)
return nki_attention.softmax_pool(score_rows, latent_rows)
ratio = score_rows.shape[1]
tile_max = score_rows[:, 0]
for row in range(1, ratio):
tile_max = torch.maximum(tile_max, score_rows[:, row])
tile_sum = torch.zeros_like(tile_max)
tile_acc = torch.zeros_like(tile_max)
for row in range(ratio):
e = torch.exp(score_rows[:, row] - tile_max)
tile_sum = tile_sum + e
tile_acc = tile_acc + latent_rows[:, row] * e
inv_sum = 1.0 / tile_sum
return (tile_acc * inv_sum).to(torch.bfloat16)
def compress_and_update_state_prefill_tl(
DIM: int,
COMPRESS_RATIO: int,
HAS_INDEX: bool,
) -> Callable[..., None]:
assert COMPRESS_RATIO in (1, 2)
ratio = COMPRESS_RATIO
def _compress_and_update_state_prefill(
score: torch.Tensor,
latent: torch.Tensor,
state_cache: torch.Tensor,
state_block_idx: torch.Tensor,
compressed_batch_mapping: torch.Tensor,
cu_seqlens_q: torch.Tensor,
cu_seqlens_k: torch.Tensor,
cu_compressed_seqlens_q: torch.Tensor,
mirror_cp_reverse_index: torch.Tensor | None,
kv_compressed: torch.Tensor,
) -> None:
assert score.shape[-1] == DIM and state_cache.shape[-1] == DIM
assert (mirror_cp_reverse_index is not None) == HAS_INDEX
device = score.device
state_size = state_cache.shape[1]
num_seqs = state_block_idx.shape[0]
total_c = compressed_batch_mapping.shape[0]
total_q = mirror_cp_reverse_index.shape[0] if HAS_INDEX else score.shape[0]
num_rows = score.shape[0]
cu_q = cu_seqlens_q.to(torch.int64)
cu_k = cu_seqlens_k.to(torch.int64)
cu_c = cu_compressed_seqlens_q.to(torch.int64)
qs_all, qe_all = cu_q[:-1], cu_q[1:]
qlen_all = qe_all - qs_all
klen_all = cu_k[1:] - cu_k[:-1]
qstart_all = klen_all - qlen_all
state_all = state_block_idx.to(torch.int64)
reverse = mirror_cp_reverse_index.to(torch.int64) if HAS_INDEX else None
def to_row(orig_idx: torch.Tensor) -> torch.Tensor:
orig_idx = orig_idx.clamp(0, max(total_q - 1, 0))
row = reverse.index_select(0, orig_idx.reshape(-1)).view(orig_idx.shape) if HAS_INDEX else orig_idx
return row.clamp(0, max(num_rows - 1, 0))
# ---- phase 1: compression (reads old state before it is overwritten) ----
if total_c > 0:
seq = compressed_batch_mapping.to(torch.int64).clamp(0, num_seqs - 1)
qs, qstart, klen = qs_all[seq], qstart_all[seq], klen_all[seq]
sidx = state_all[seq]
total_new_blocks = klen // ratio - qstart // ratio
block_id = torch.arange(total_c, dtype=torch.int64, device=device) - cu_c[seq]
valid = (block_id >= 0) & (block_id < total_new_blocks)
block_start = (qstart // ratio + block_id) * ratio
pos = block_start[:, None] + torch.arange(ratio, dtype=torch.int64, device=device)[None, :] # [C, R]
from_cache = pos < qstart[:, None]
cache_pos = torch.remainder(pos, state_size) # pos >= 0 for valid blocks (truncmod == remainder)
cache_slot = sidx[:, None].expand_as(pos)
cached = state_cache[cache_slot, cache_pos] # [C, R, 2, D]
rows = to_row(pos - qstart[:, None] + qs[:, None])
s_rows = torch.where(from_cache[..., None], cached[:, :, 0], score[rows].float())
l_rows = torch.where(from_cache[..., None], cached[:, :, 1], latent[rows].float())
result = _compress_rows(s_rows, l_rows)
kv_compressed.copy_(torch.where(valid[:, None], result, torch.zeros_like(result)))
# ---- phase 2: store the last min(qlen, state_size) tokens of every sequence ----
if total_q == 0 or num_seqs == 0:
return
tok = torch.arange(total_q, dtype=torch.int64, device=device)
seq_t = torch.searchsorted(qe_all, tok, right=True)
tok_valid = seq_t < num_seqs
seq_t = seq_t.clamp_max(num_seqs - 1)
rel = tok - qs_all[seq_t]
qlen_t = qlen_all[seq_t]
# Tokens older than the last ``state_size`` ones are redirected to the stored token occupying the
# same slot, so duplicate writes carry identical values (no masking / host sync needed).
first_stored = (qlen_t - state_size).clamp_min(0)
lag = (first_stored - rel).clamp_min(0)
rel_src = rel + (lag + state_size - 1) // state_size * state_size
# Rows past cu_seqlens_q[-1] (if any) duplicate the write of the globally last token.
anchor = (qe_all[-1] - 1).clamp_min(0)
anchor_seq = torch.searchsorted(qe_all, anchor[None], right=True).clamp_max(num_seqs - 1)[0]
src_tok = torch.where(tok_valid, qs_all[seq_t] + rel_src, anchor)
seq_w = torch.where(tok_valid, seq_t, anchor_seq)
rel_w = src_tok - qs_all[seq_w]
any_token = qe_all[-1] > 0
slot_w = state_all[seq_w]
pos_w = torch.remainder(qstart_all[seq_w] + rel_w, state_size)
rows = to_row(src_tok)
new_vals = torch.stack((score[rows].float(), latent[rows].float()), dim=1) # [T, 2, D]
old_vals = state_cache[slot_w, pos_w]
vals = torch.where(any_token, new_vals, old_vals)
state_cache.index_put_((slot_w, pos_w), vals.to(state_cache.dtype))
return _compress_and_update_state_prefill
def compress_and_update_state_decode_tl(
DIM: int,
COMPRESS_RATIO: int,
) -> Callable[..., None]:
assert COMPRESS_RATIO in (1, 2)
ratio = COMPRESS_RATIO
def _compress_and_update_state_decode(
score: torch.Tensor,
latent: torch.Tensor,
state_cache: torch.Tensor,
state_block_idx: torch.Tensor,
context_lens: torch.Tensor,
kv_compressed: torch.Tensor,
) -> None:
assert score.shape[-1] == DIM and state_cache.shape[-1] == DIM
total_q = score.shape[0]
if total_q == 0:
return
device = score.device
state_size = state_cache.shape[1]
sidx = state_block_idx.to(torch.int64)
klen = context_lens.to(torch.int64)
abs_pos = klen - 1
# Store all tokens before computing (windows may include tokens of this step).
cache_pos = torch.remainder(abs_pos, state_size)
vals = torch.stack((score.float(), latent.float()), dim=1) # [Q, 2, D]
state_cache.index_put_((sidx, cache_pos), vals.to(state_cache.dtype))
is_end = torch.remainder(abs_pos, ratio) == ratio - 1
block_start = abs_pos.div(ratio, rounding_mode="floor") * ratio
pos = block_start[:, None] + torch.arange(ratio, dtype=torch.int64, device=device)[None, :]
win = state_cache[sidx[:, None].expand_as(pos), torch.remainder(pos, state_size)] # [Q, R, 2, D]
result = _compress_rows(win[:, :, 0], win[:, :, 1])
kv_compressed.copy_(torch.where(is_end[:, None], result, torch.zeros_like(result)))
return _compress_and_update_state_decode