This document is relevant for: Trn2, Trn3
High-performance GPT-OSS 120B implementation for 8K/1K serving on Trn3#
Starting with Neuron SDK 2.33, vLLM Neuron has a high-performance implementation of GPT-OSS 120B for 8K-input, 1K-output serving on Trn3. The implementation includes features such as LNC=1 decode and a decode megakernel, among other optimizations. The megakernel fuses all decoder layers of a decode step into one NKI kernel. The implementation runs as a disaggregated deployment, with six prefill replicas and one decode server on two Trn3 instances. The Overview explains each part, and the steps run the configuration with self-contained scripts.
Known limitations:
The implementation supports Trn3 with MXFP4 weights only.
The megakernel runs only on the decode server of a disaggregated deployment. A single-instance server and the prefill servers use the per-layer path at LNC=2.
The tested shape is 8,192 input tokens and 1,024 output tokens. The servers use
--max-model-len 10240, a block size of 128, and an FP8 KV cache.The decode server uses
--max-num-seqs 8on each of its 16 data-parallel ranks. This gives 128 concurrent requests for each decode instance.
Use this tutorial for GPT-OSS 120B with MXFP4 weights on Trn3 at 8K/1K. For a different case, use the gpt-oss tutorial. It covers gpt-oss 20B, Trn2 with BF16 weights, a single-instance deployment, and a context of up to 16,384 tokens.
Overview#
The configuration is a disaggregated-inference (DI) deployment with six prefill replicas and one decode server (6P1D). It runs on two Trn3 instances.
Node |
Role |
Servers |
Parallelism |
Logical NeuronCores |
|---|---|---|---|---|
A |
Prefill |
6 replicas on ports 8100 to 8105 |
TP4 DP1 EP2 for each replica, at |
24 of 64 |
A |
Proxy and benchmark client |
Proxy on port 8000 |
||
B |
Decode |
1 server on port 8200 |
TP8 DP16 EP128, at |
128 of 128 |
Note
The prefill instance of this deployment has idle NeuronCores. The six replicas use 24 of its 64 logical NeuronCores. For production, keep six prefill replicas for each decode instance, and fill each prefill instance with 16 replicas. Three prefill instances and eight decode instances use every NeuronCore. That deployment has 48 prefill replicas and 8 decode servers on 11 instances.
The decode server differs from the GPT-OSS DI deployment in the gpt-oss tutorial in these points:
Part |
What it does |
|---|---|
LNC=1 decode |
|
Decode megakernel |
One NKI kernel runs all decoder layers of a decode step. vLLM Neuron selects it automatically for an MXFP4 decode server at LNC=1 on Trn3. No flag turns it on. |
Packed FP8 KV cache |
|
Fused token embedding |
The megakernel builds the token embedding ( |
Fused sampling tail |
The final RMSNorm, the lm_head, the top-k and the sampler run in one chunked kernel, with one 128-rank all-to-all ( |
DMA-order schedule |
A schedule file sets the order of the DMA transfers inside the megakernel ( |
Host-side settings |
Four batches in flight ( |
The scripts of this tutorial are in the vLLM Neuron repository, in
examples/vllm_neuron/models/gpt_oss/120b/mxfp4/multinode_DI_8k1k/.
They launch each part with the tested configuration. The directory is
self-contained: it carries the DMA-order schedule and the helper that prepares
it.
File |
Where to run |
Purpose |
|---|---|---|
|
Sourced by every script |
The addresses, the shape, the ports, and the environment that both roles need |
|
Node B |
The megakernel decode server ( |
|
Node A |
One prefill replica ( |
|
Node A |
All six replicas, one after the other |
|
Node A |
The example proxy |
|
Node A |
A health check and one completion through the proxy |
|
Node A |
The two benchmark legs: a warmup leg and the measured leg |
|
Called by |
Writes the schedule for the kernel-call scope of the active compile route |
|
Read by the helper |
The DMA-order schedule of the decode megakernel for Neuron SDK 2.33 |
Before you start#
This tutorial assumes that you have worked through:
The gpt-oss deployment tutorial. It explains the GPT-OSS DI deployment and its flags.
The disaggregated inference tutorial. It explains the NIXL KV connector, the proxy, and the EFA setup.
Tested versions#
Component |
Version |
|---|---|
Neuron SDK |
2.33 |
vLLM Neuron plugin |
Shipped with the Neuron SDK 2.33 release |
vLLM (upstream) |
0.29.0 |
gpt-oss 120B checkpoint |
|
Prerequisites#
Two Trn3 instances with EFA connectivity. Put them in the same subnet and placement group. The security group must allow this traffic between the two instances: port 8000 (proxy), ports 8100 to 8105 (prefill replicas), port 8200 (decode), the NIXL side-channel ports 5559, 5659, 5759, 5859, 5959 and 6059 (prefill) and 8759 (decode), and all EFA traffic (a self-referencing rule).
Neuron SDK 2.33 with vLLM Neuron on both instances. See the setup guide. Install
nixlwith its LIBFABRIC runtime libraries, as the DI tutorial describes.The vLLM Neuron examples on both instances. Clone the vLLM Neuron repository, or copy its
examples/vllm_neuron/directory. The scripts are inexamples/vllm_neuron/models/gpt_oss/120b/mxfp4/multinode_DI_8k1k/, and the proxy is inexamples/vllm_neuron/vllm/disaggregated_inference/.The checkpoint on a local NVMe volume of both instances. Each server loads the checkpoint at startup. A local NVMe volume gives a faster startup than a network volume or a download at startup. Download
openai/gpt-oss-120bfrom Hugging Face to the same path on both instances, and setMODEL_IDto that path in step 1. Put the compile cache on local NVMe too, as the setup guide describes.hf download openai/gpt-oss-120b --local-dir /path/to/nvme/gpt-oss-120b
Step 1: Set the addresses#
In this step, you tell the scripts where the two nodes are.
On both nodes, export the private IP of each node:
export PREFILL_HOST=10.0.1.10 # node A
export DECODE_HOST=10.0.1.20 # node B
export MODEL_ID=/path/to/nvme/gpt-oss-120b # or openai/gpt-oss-120b
Replace the two addresses with the private IPv4 addresses of your instances.
Every script sources env_common.sh. That file reads these variables and
exports the environment that both roles need. Every other value has a default.
Open the file to see each value and the reason for it.
The logs of the servers go to ~/gpt-oss-120b-8k1k-results/logs/, and the
benchmark results go to ~/gpt-oss-120b-8k1k-results/. Set BENCH_RESULT_DIR
or SERVER_LOG_DIR to change the location.
Step 2: Launch the decode server on node B#
In this step, you start the one decode server. It takes every NeuronCore of the node.
cd examples/vllm_neuron/models/gpt_oss/120b/mxfp4/multinode_DI_8k1k
./run_decode.sh
The script does three things before it calls vllm serve:
It sets
NEURON_LOGICAL_NC_CONFIG=1and the decode-only environment.It writes the DMA-order schedule for the decode graph into
MEGAKERNEL_SCHEDULE_DIR(default~/.cache/vllm-neuron/megakernel_schedule), and it exports the path of the file. The path is part of the compile-cache key, so keep one directory.It exports
VLLM_NEURON_MEGA_FUSE_EMBED=1andVLLM_NEURON_TAIL_FLAT_ROW_GATHER=1.
The first launch compiles the decode graph. Later launches use the compile
cache. The server reports Application startup complete when it is ready. You
can start the prefill replicas (step 3) at the same time.
Step 3: Launch the six prefill replicas on node A#
In this step, you start six TP4 prefill servers. Replica i binds NeuronCores
4i to 4i+3, serves HTTP on port 8100 + i, and uses the NIXL side-channel
port 5559 + 100 * i.
cd examples/vllm_neuron/models/gpt_oss/120b/mxfp4/multinode_DI_8k1k
./run_prefill_fleet.sh
The fleet script starts replica 0 and waits until it answers /health, so the
compile cache holds the prefill graph. Then it starts replicas 1 to 5 the same
way. Replica 0 compiles the prefill graph, so it takes the longest to start.
The later replicas use the compile cache. The replicas keep running after the
script exits. Their logs are in
~/gpt-oss-120b-8k1k-results/logs/prefill_<i>.log. To start one replica by
hand, run ./run_prefill.sh <index>.
The six replicas are six separate TP4 DP1 EP2 servers, and not one DP6 server. Expert parallelism cannot share data-parallel ranks on the prefill graph.
Step 4: Launch the proxy on node A#
In this step, you start the proxy and send one request through it. Start the
proxy after the decode server and all six replicas report
Application startup complete:
./run_proxy.sh &
./run_client.sh
The health check of the proxy (/healthcheck) returns HTTP/1.1 200 OK, and
the completion returns an OpenAI-compatible JSON payload with a coherent
choices[0].text.
Note
The proxy is toy_proxy_server.py from the vLLM Neuron examples. It sends the
prompts to the prefill replicas in turn (round robin). It is a minimal example
router, and not a production router. For production, use a production router.
Step 5: Run the benchmark#
In this step, you run two benchmark legs against the proxy. Both legs send
random prompts of 8,192 tokens, ask for 1,024 output tokens with
--ignore-eos, and keep 128 requests in flight. 128 is the decode capacity: 8
sequences on each of the 16 data-parallel ranks.
./run_bench.sh
Leg |
Prompts |
Purpose |
|---|---|---|
Warmup |
1,280 |
The first leg after a server launch is not a valid measurement. Discard it. |
Measured |
12,800 |
The result. The script prints the key metrics and keeps |
Before the measured leg sends its first request, the client generates the
12,800 random prompts of 8,192 tokens. That step takes several minutes, and the
servers are idle while it runs. Read these keys of bench_clean.json:
median_tpot_msandp99_tpot_ms: the time for each output token, as the client sees it.1000 / median_tpot_msis the output tokens per second for each user.output_throughput: the output tokens per second over all 128 users.total_token_throughput: the input plus output tokens per second.mean_ttft_msandp99_ttft_ms: the time to the first token. The prefill replicas and the KV transfer set it.
Configuration reference#
Shape#
These values are the tested configuration of the megakernel decode.
Setting |
Value |
Reason |
|---|---|---|
|
10240 |
Holds the 8,192 input tokens and the 1,024 output tokens. |
|
8192 |
One prefill segment holds the whole prompt. Both roles use one bucket, |
|
128 |
The KV block size of the packed FP8 cache. |
|
Both roles |
The packed FP8 KV layout that the prefill writes and that the megakernel reads. |
|
8 |
The partition axis caps the per-rank decode batch at |
|
1 |
One prompt for each prefill step. |
|
16 TP8 ranks give 128 ranks, with one expert on each rank. |
|
|
The megakernel builds the token embedding, so the embedding is not sharded over the DP group. |
|
|
The lm_head is replicated over the 16 data-parallel ranks. |
|
|
One 128-rank all-to-all in the fused tail. |
|
|
The compiler optimization level of the decode graph. |
|
|
Both roles |
No logprob gather. |
|
Both roles |
Keeps the hybrid KV cache manager that the sliding-window layers need. |
|
Both roles |
Lets vLLM load the published MXFP4 checkpoint. |
|
Both roles |
The multiprocess executor. |
Environment#
Both roles, from env_common.sh:
Variable |
Value |
Reason |
|---|---|---|
|
|
The compile route. libtorch-neuronx-lite reads this variable itself, and it selects its native-route setup only for an explicit value. |
|
|
The native route needs the graph-capture backend off. |
|
|
Turns off the vocab-sharding FX pass, so the per-rank vocab offset stays out of the graph. |
|
|
Packed-FP8 KV writes benign out-of-bound skip notifications. Without this setting, the notifications stop the decode server on a long run. |
|
|
The NIXL progress thread polls with no delay by default and takes a core from the worker. |
|
|
The side channel must accept the KV handshake from the other node. |
|
|
The limit in seconds for the compile of one graph. The default is 600, and the compile of one prefill graph takes several minutes. |
|
|
The limit in seconds for the startup of one server. The default is 600, and the first prefill replica compiles its graph during the startup. |
Decode only, from run_decode.sh:
Variable |
Value |
Reason |
|---|---|---|
|
|
The megakernel needs one logical NeuronCore for each physical core. |
|
|
Keeps the source tensors of a collective off shared HBM. A toolchain that requires this stops the trace without it. |
|
|
Four batches in flight hide the host time of a step. |
|
|
The completion poll backs off and gives host cores back to the workers. |
|
The schedule path |
The DMA-order schedule. When this variable is set, vLLM Neuron also sets the kernel options that the schedule was made with. |
|
|
The token embedding inside the megakernel. |
|
|
One row gather in the fused tail. |
|
|
Past the last prefill side-channel port. |
Prefill only, from run_prefill.sh:
Variable |
Value |
Reason |
|---|---|---|
|
|
The prefill graph runs the per-layer path at LNC=2. |
|
|
The four cores of replica |
|
|
One side-channel port for each replica. The same value goes into |
Script options:
Variable |
Default |
Purpose |
|---|---|---|
|
|
A Hugging Face model ID, or the local path of the checkpoint. |
|
|
The benchmark results. |
|
|
The logs of the servers and of the proxy. |
|
The JSON file of this directory |
The shipped DMA-order schedule. |
|
|
Where |
|
|
The kernel-call scope of the schedule. |
|
The proxy of the examples tree |
The path of |
|
|
The address that the proxy listens on. |
|
|
The proxy address for |
|
|
The netrc file that the benchmark client reads before each request. Keep a local path. See Common issues. |
Common issues#
The benchmark reports a low throughput and a high TTFT, but the servers are healthy#
The benchmark client can be the bottleneck. vllm bench serve opens its HTTP
session with trust_env, so the client looks up ~/.netrc before it sends each
request. On a network file system, each lookup can take hundreds of
milliseconds. The client then sends requests late. The request throughput
decreases and the TTFT increases, but the TPOT and the server metrics stay
normal.
run_bench.sh points the lookup at /dev/null (BENCH_NETRC). If you run
vllm bench serve yourself, set NETRC=/dev/null on the command. You can also
run the client as a user whose $HOME is on a local disk.
The decode TPOT is much larger than expected#
Start the decode server with run_decode.sh. The script sets the megakernel
tuning. Each setting is off by default, and without them the server gives
correct output at a larger step time.
After a change to a kernel option or to the schedule file, clear the compile caches on both nodes. The cache key does not include the content of the schedule file or the kernel options.
rm -rf ~/.cache/neuron_libtorch ~/.cache/vllm/neuron /var/tmp/neuron-compile-cache /var/tmp/nki-*cache*
The decode trace stops with Partition dim size must be preserved#
--max-num-seqs on the decode server is above 8. Keep 8 for TP8 and EP128.
A server stops during compilation#
A compile or a startup took longer than its limit. Raise
NEURON_LIBTORCH_COMPILATION_TIMEOUT, the limit for one graph, and
VLLM_ENGINE_READY_TIMEOUT_S, the limit for the startup of one server, in
env_common.sh.
Requests stall after the prefill, or the NIXL handshake fails#
Check VLLM_NIXL_SIDE_CHANNEL_HOST=0.0.0.0 on both roles. Check the
side-channel ports in the security group: 5559 to 6059 in steps of 100 for the
replicas, and 8759 for the decode server. Check the EFA rule. Enable the NIXL
logs as the disaggregated inference tutorial
describes.
The decode server stops on a long run with many out-of-bound skip notifications#
NEURON_RT_DBG_VECTOR_DGE_SKIP_NOTIF=1 is absent on one role. env_common.sh
sets it for both roles.
Clean up#
Stop each server with SIGINT, so it shuts down in order:
pkill -INT -f "vllm serve" # on both nodes
pkill -f toy_proxy_server # on node A