This document is relevant for: Trn2, Trn3
GPT-OSS-120B giga-kernel decode#
The giga-kernel (GK) fuses the entire GPT-OSS-120B decoder stack (all 36
layers: attention + MoE) into a single decode NEFF, replacing the per-layer
decode loop. It is decode-only, runs at logical-NC config 1 (lnc1), and
uses a packed-FP8 KV cache. The target deployment is disaggregated inference
(DI): a native per-layer prefill engine produces KV, which is transferred
over NIXL to a GK decode engine (TP8 × DP16 × EP128).
This document describes the integration as it stands: the model, runner, and kernel seams, the per-buffer KV-cache layout, and the DI wiring.
Why decode-only + DI#
The kernel is a single lnc1 NEFF (grid[1]). A per-layer prefill graph uses
lnc2 and lowers TP/EP SendRecv collectives that do not lower at lnc1
(NCC_IBTN243). A fused single-engine server (prefill + decode in one vllm serve, lnc1) therefore cannot compile. GK is architecturally a DI decode
component:
Prefill role — native per-layer
model_mxfp4(TP4/EP2/lnc2),kv_role=kv_producer.Decode role — GK
model_mxfp4_perf(TP8/DP16/EP128/lnc1, packed-FP8),kv_role=kv_consumer.
The two roles run as independent single-node engines wired by IP over NIXL/LIBFABRIC.
Component map#
Component |
File |
Responsibility |
|---|---|---|
Model factory gate |
|
selects the GK perf model when |
GK decode model |
|
decode-only weight holders; |
Kernel assembly |
|
stacks per-layer weights once; builds attention-control tensors; passes the per-buffer KV; calls the kernel |
Kernel entry |
|
wraps |
KV allocation |
|
GK-gated per-buffer |
NIXL connector |
|
per-buffer registration; DCP read logic; SWA DI truncation |
Greedy sampler |
|
lnc-matched distributed argmax |
The GK path is confined to the perf model + its runner overrides; non-GK models are untouched by the model/kernel changes.
Kernel contract#
The kernel (gpt_oss_decode_mega_kernel, an nkilib c128_giga_kernel fork)
consumes, per decode step:
Weights, stacked once onto a leading
[num_layers, …]axis in the exact per-layer shapes the kernel indexes asW[i](see the layout list inmega_decode.py). Each stack is built once on the host and loaded to the device directly by its own weight loader (build_stacked_weight_specs), so the per-layer copies never reach the device. Stacking is not inside the traceddecode(); restacking in-graph captured an ~810 MBconcatenateper step.KV cache:
group_sizeper-group base-0 buffers, passed as separate K and V args (see “KV-cache layout”). The kernel also accepts the stackedK_cache/V_cacheand asserts exactly one form is present; it reads the packed-FP8 path from the K buffer rank.Attention control (layer-invariant, built from per-layer
attn_metadata): RoPE cos/sin, a full block table + a trimmed-SWA block table, active mask, absolute + window-frame positions, and two KV-update indices (kv_cache_update_idx+kv_cache_update_idx_swa).TP and EP rank meshes as tuples-of-tuples (two
ReplicaGroupargs).
GPT-OSS alternates even = sliding-window (SWA, window 128) and odd = full
attention layers. The kernel bakes this parity: it selects the block table and
write index per layer by i % 2, and indexes the KV cache buffer by i // 2
(paired layers 2j, 2j+1 share buffer j). Weights stay indexed by i; only
the KV-cache axis is group_size.
KV-cache layout (per-buffer, HMA-preserving)#
vLLM’s Hybrid KV-cache Manager (HMA) reports GPT-OSS as 2 groups (SWA + full)
and packs the 36 per-layer specs into group_size = 18 shared buffers (each
buffer shared by one SWA + one full layer, shared_by = [2i, 2i+1]). num_blocks
is a single global scalar; all buffers are identical size.
get_kv_spec stays honest and per-layer (SWA layers report their window;
full layers report full attention) so HMA stays enabled and SWA keeps its reduced
per-request block-ID consumption. The GK-gated initialize_kv_cache branch
allocates one separate tensor per HMA group:
group_bufs[i] : [2, num_blocks, kv, block, d] # K half, then V half, per group i
K_i = group_bufs[i][0] # kernel per-buffer K arg (packed-FP8: a trailing .view)
V_i = group_bufs[i][1] # kernel per-buffer V arg
Each group is its own allocation the kernel indexes base-0, so the block-KV DMA
offset spans one group and stays under uint32 with no cap. Packed-FP8 K is a
trailing .view([…, block//2, d, 2]) on group_bufs[i][0], same bytes and same
num_blocks. V is never packed.
The buffers are handed to the kernel via a GK-only set_kv_cache_buffers,
which stores each as an individual model attribute (K_cache_buf_i /
V_cache_buf_i) rather than one list. A list would not survive the fork-child
meta-swap, which walks tensor attributes but does not recurse into a list, so the
buffers would stay on the real device and fail the meta-only compile. Per-layer
bind_kv_cache({layer: [k, v]}) keeps the generic signature every model shares;
packed-ness is inferred from k.dim() == v.dim() + 1.
Each group buffer is [2, num_blocks, …] in its own storage, so the per-layer DI
region is that whole buffer and the NIXL connector registers each group’s K and V
directly.
DI wiring (NIXL)#
The problem. The transfer must land each prefill KV block on the group buffer
block the kernel reads, matching what the native per-layer prefill registers.
vLLM’s stock _register_packed_kv_cache assumes a block-major,
all-layers-interleaved (“DSv4 packed”) layout and would address the wrong bytes.
The fix. For the GK decode (gk_decode set, kv_role != kv_producer),
NeuronNixlConnectorWorker._register_packed_kv_cache registers per-buffer K/V
regions, one per group buffer’s K and one per group buffer’s V, each
block_len = kv·block·d (one layer-pair block), de-duplicating buffers shared by
paired layers via base address. This reproduces what a native per-layer prefill
registers, so the handshake sees head_ratio = 1 (match path) and a NIXL
descriptor base_region + b·block_len lands on that buffer’s block b. A non-GK
cache defers to super().
The prefill role runs the native per-layer model (gk_decode=False), which
registers the same per-buffer region set natively — so P and D region counts and
block-lens match with no producer-side change.
The connector also carries a sliding-window DI truncation fix
(NeuronNixlConnectorScheduler): on a hybrid SWA model the decode node admits
the prompt at length N but recomputes the last token at N-1, so the SWA skip
evicts the window’s oldest block one step early at the N % block_size == block_size-1 boundary and that block is never transferred. Both nodes are made
to operate at N-1. This is independent of the GK KV layout.
KV budget (no cap)#
Each group buffer is indexed base-0, so the block-KV DMA HWAPOffset spans one
group’s num_blocks · kv_heads · block · head_size, which stays under uint32
(NCC_IBTN127) at any pool size the HBM holds. No GK-specific KV-budget cap is
needed; num_blocks is the standard profiled budget. neuron_worker.py no longer
adjusts it.
For the 120B GK config (group_size=18, kv_heads=1, block=128,
head_size=64) the profiled budget is num_blocks ≈ 36,175 per engine
(≈9.94 GiB KV, ~2.54M KV tokens), about 2x the count the earlier stacked uint32
cap allowed. The KV budget is now bounded by gpu_memory_utilization, not by any
GK offset limit.
This document is relevant for: Trn2, Trn3