Skip to content

Commit 69cd243

Browse files
committed
[TRTLLM-16460][doc] trtllm_modeling_v2: design doc, user and developer guides, examples, results and reference docs
- README.md: the pipeline, the gates, the execution substrate, tuning, lessons and every flag. - docs/: the documentation set: an index, design.md (goals, principles, architecture, lifecycle, checks, task graph, substrate, tuning, decisions, limitations), user-guide.md, developer-guide.md, examples.md (an end-to-end worked run) and results.md (measured results and what is not measured yet). - INTAKE.md: the user-facing task.yaml, site.yaml and the fast path. - TROUBLESHOOTING.md: keyed on the verbatim error, including the environment failures that read as model failures. - BUILD_AND_TEST.md: building and testing at each scale. - LESSONS.md: what real runs got wrong, and the mechanism that now prevents each. - AGENTS.md gains a short table of environment failures that read as model failures; agent-flow/README.md lists the workflow. Signed-off-by: Wanli Jiang <35160485+Wanli-Jiang@users.noreply.github.com>
1 parent 6bc4e94 commit 69cd243

17 files changed

Lines changed: 9346 additions & 1 deletion

File tree

‎AGENTS.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,21 @@ For a full list of up-to-date bot commands, post `/bot help` as a PR comment and
193193

194194
- Use `TLLM_LOG_LEVEL_BY_MODULE` to enable per-module log filtering (e.g., `"debug:_torch,runtime;info:serve"`); see [Module-Level Logging](docs/source/developer-guide/overview.md#module-level-logging) for details.
195195

196+
**Environment failures that read as model failures.** Each of these presents as a broken
197+
model or a broken build, and none of them is. Check them before debugging modeling code:
198+
199+
| symptom | cause | fix |
200+
|---|---|---|
201+
| `MPI_ERR_SPAWN: could not spawn` | the container runtime forbids `MPI_Comm_spawn`, which the default LLM API path uses to start workers (normal under pyxis/enroot) | start one task per rank under `trtllm-llmapi-launch` so the ranks exist before the engine; `TLLM_WORKER_USE_SINGLE_PROCESS=1` only at world size 1 |
202+
| `Impossible to fit in any sequence in kvCache` | the engine inherited the checkpoint's `max_position_embeddings` (262144 is common) and sized the cache for it | pass `--max_seq_len` / `--max_batch_size` / `--kv_cache_fraction` explicitly |
203+
| `NVRTC ... could not open source file "cuda.h"` | usually the venv's `bin` missing from `PATH`; separately, the generation FMHA kernel takes the NVRTC path unless the KV dtype is `E2m1` (grep `options.mDtypeKv != tg::Dtype::E2m1`) | check `PATH` first; `--kv_cache_dtype fp8` takes the non-NVRTC branch |
204+
| the engine hangs at startup and never returns | a wrong launch substrate hangs rather than raises — typically an MPI connect-back on an interface that does not carry it | probe with a deadline rather than waiting |
205+
| the import resolves to a different checkout | `/root/.local` is shared across container runs on some clusters | `PYTHONNOUSERSITE=1` at runtime — but **not** during the native build, which needs the user site for `cutlass_library` |
206+
207+
A longer list, with the wrong diagnoses recorded alongside the right ones, is in
208+
[`agent-flow/agent_flow/workflows/trtllm_modeling_v2/TROUBLESHOOTING.md`](agent-flow/agent_flow/workflows/trtllm_modeling_v2/TROUBLESHOOTING.md).
209+
It is keyed on verbatim error text, so search it for the string you saw.
210+
196211
## Key Documentation
197212

198213
| Topic | Path |

‎agent-flow/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,5 @@ block, before `run`: it writes each role's composed system prompt verbatim to
297297
handed in, since that is what the agents are built from (`AgentTeamWorkflow`
298298
appends its MCP-tool protocol to the bundle it is given). The `perf-analyze`
299299
and `perf-optimize` CLIs use it to snapshot `<workspace>/prompts/` at every
300-
launch.
300+
launch; `trtllm-modeling-v2` does the same from preflight, once the launch
301+
environment its prompts carry is resolved.
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Build, rebuild, and test at each scale
2+
3+
A companion to [TROUBLESHOOTING.md](TROUBLESHOOTING.md). That file is keyed on
4+
symptoms — *something broke, what is it*. This one is keyed on tasks — *I need
5+
to build / rebuild / run something at N GPUs, how*.
6+
7+
Most of it is not this workflow's knowledge. It is collected from the places
8+
in TensorRT-LLM that already do these things correctly, each named so you can
9+
read the current version rather than trust this page:
10+
11+
| where | what it owns |
12+
|---|---|
13+
| `scripts/build_wheel.py` | the build itself, and every flag below |
14+
| `.claude/skills/exec-slurm-compile/` | building on a Slurm cluster, and `enroot import` |
15+
| `.claude/skills/trtllm-test-script-builder/` | generating Slurm test scripts — **the launch rules** |
16+
| `.claude/skills/exec-local-slurm/` | persistent allocations, container reuse, step cancellation |
17+
| `tests/integration/test_lists/test-db/*.yml` | which tests run at which GPU count |
18+
| `scripts/test_to_stage_mapping.py` | which CI stage a given test lands in |
19+
20+
---
21+
22+
## 0. First ask: do you need to build at all?
23+
24+
**For a modeling_v2 onboard, almost certainly not.** This is worth checking
25+
before spending an hour, because the answer is usually no:
26+
27+
| what you changed | rebuild? |
28+
|---|---|
29+
| a target's `modeling.py` / `weights.py` | **no** — pure Python, editable install picks it up |
30+
| a catalog entry `.py`, shared or in a target's `ops/` | **no** |
31+
| a FlashInfer call (sourcing tier 3) | **no** — the package is installed, its kernels JIT |
32+
| a Triton kernel (sourcing tier 5) | **no** — Triton is JIT |
33+
| routing, `_router_index.py` | **no** |
34+
| anything under `cpp/` | yes |
35+
| a new `torch.ops.trtllm.*` op | yes |
36+
| pulling a trunk that touched `cpp/` | yes |
37+
38+
The five sourcing tiers this workflow uses — an exported op, the kernel plane,
39+
FlashInfer's public API, native torch, Triton — **none of them require a native
40+
rebuild**. That is why
41+
the orchestrator never builds anything and `environment.python` just points at
42+
an install someone else made.
43+
44+
If you are rebuilding during an onboard, stop and confirm why.
45+
46+
### Rebuilding while compute sessions are running
47+
48+
`build_wheel.py` installs `tensorrt_llm/libs/*.so` by rewriting each file in
49+
place. Any process, on any node, that has the old file mapped then faults on
50+
its next page-in, and when that process is attached to its allocation's MPI it
51+
takes the whole allocation with it. Measured: a rebuild on one session killed
52+
another session thirty seconds later.
53+
54+
So rebuild through the workflow's `incremental_build.sh` (in the package's
55+
`scripts/` directory), as a whole-node spool job:
56+
57+
```bash
58+
spoolctl.py submit --gpus all --wall 3h build-<what> \
59+
'bash <package>/scripts/incremental_build.sh --repo <checkout> --spool <spool A> --spool <spool B>'
60+
```
61+
62+
It takes a per-checkout build lock, announces the rebuild on every listed
63+
spool (each session's scheduler then holds new GPU jobs against that checkout),
64+
gives every installed library a fresh inode by copy-and-rename so running
65+
processes keep the file they mapped, runs the build with ccache, checks the
66+
checkout still imports, and retracts the announcement — also when the build
67+
fails. Spools default to `$MV2FLOW_SESSION_SPOOLS`, which every rendered
68+
session exports.
69+
70+
## 1. Build
71+
72+
```bash
73+
python3 ./scripts/build_wheel.py \
74+
--use_ccache \
75+
--skip_building_wheel \
76+
--cuda_architectures "100-real;103-real"
77+
```
78+
79+
| flag | when |
80+
|---|---|
81+
| `--cuda_architectures` | **always set it.** `100-real` Blackwell B200/GB200, `103-real` GB300, `90-real` Hopper, `89-real` L40S, `80-real` A100. Building all of them is the slowest thing you can do |
82+
| `--use_ccache` | always. A no-op change rebuilds in minutes instead of an hour |
83+
| `--skip_building_wheel` | development — builds in place, no wheel artifact |
84+
| `--fast_build` / `-f` | skips some kernels. Development only — **do not gate on a fast build** |
85+
| `--clean` / `-c` | wipes the build directory. The answer to "weird compile issues" |
86+
| `--configure_cmake` | force a CMake re-configure. Needed when a CMake input changed but the build did not notice |
87+
| `-j <N>` | lower it when the build OOMs |
88+
89+
`build_wheel.py` creates its own venv and runs `pip install -e .[devel]` into
90+
it, so the interpreter it produces is the one to put in `environment.python`.
91+
92+
### ccache does not carry across checkouts
93+
94+
Measured: pointing `CCACHE_DIR` at another checkout's cache produced no
95+
useful hit rate. Absolute paths are baked into the cache keys. Give each
96+
checkout its own, and expect the first build there to be cold.
97+
98+
### The one that wastes an afternoon
99+
100+
A rebuild is *not* the fix for `NVRTC ... could not open source file "cuda.h"`.
101+
Measured: rebuilding with `--nvrtc_dynamic_linking` (98% ccache hits) did not
102+
fix it; the cause was the venv's `bin` missing from `PATH`. See
103+
TROUBLESHOOTING.md before you rebuild for that symptom.
104+
105+
## 2. Build on Slurm
106+
107+
`.claude/skills/exec-slurm-compile/` owns this end to end — `submit_compile.sh`
108+
submits, `compile.slurm` allocates, `compile.sh` runs the build inside the
109+
container, `enroot-import` fetches the image.
110+
111+
Two failure modes from its own troubleshooting table that are not obvious:
112+
113+
- `enroot import` **auth** failure → `~/.config/enroot/.credentials`;
114+
- `enroot import` producing an **empty or corrupt** `.sqsh` → the image URL
115+
probably has an `https://` prefix it should not have.
116+
117+
### The environment hazard on a shared cluster
118+
119+
`/root/.local` and `/root/.cache` may be symlinked into a shared home, so
120+
another checkout's develop-install is on the path of every container run.
121+
122+
- **At runtime: `PYTHONNOUSERSITE=1`.** Without it an import can resolve to a
123+
different tree than the one you are measuring.
124+
- **During the native build: leave the user site enabled.** The CUTLASS kernel
125+
CMake runs `setup_library.py develop --user` and then imports
126+
`cutlass_library`. Isolate the `pip install -e` step and everything after
127+
it, not the compile itself.
128+
129+
## 3. Test — the scale ladder
130+
131+
Scale is declared by `system_gpu_count` in each list's `condition` block, and
132+
the file name follows it:
133+
134+
| scale | list | `system_gpu_count` | what belongs here |
135+
|---|---|---|---|
136+
| **CPU** | `l0_cpu.yml` | `0–0` | anything that needs no device. `backend: generic` |
137+
| **1–4 GPU** | `l0_gb300.yml`, `l0_b200.yml`, `l0_h100.yml`, … | `1–4` | the ordinary pre-merge matrix |
138+
| **multi-GPU** | `l0_*_multi_gpus.yml` | `4–4`, `8–8` | collectives, TP/EP topologies |
139+
| **multi-node** | `l0_*_multi_nodes*.yml` | `8–8`+ | anything spanning nodes |
140+
141+
To find where a test you care about runs:
142+
143+
```bash
144+
python scripts/test_to_stage_mapping.py --tests "test_modeling_v2_allgather"
145+
```
146+
147+
### CPU
148+
149+
`agent-flow`'s own suite is the clearest example: pure CPU, its own stage
150+
(`CPU-AgentFlow-UnitTest`), and `AgentFlowRule` narrows CI so an
151+
agent-flow-only change runs exactly that stage and no GPU stage at all.
152+
153+
For the engine, CPU tests are the schema / list / prose audits — including the
154+
modeling_v2 claims audits, which are text scans and need no device.
155+
156+
### Single GPU
157+
158+
```bash
159+
pytest tests/unittest/_torch/modeling_v2/norm/test_modeling_v2_flashinfer_rmsnorm.py
160+
LLM_MODELS_ROOT=/path/to/models pytest tests/integration/defs/accuracy/test_modeling_v2_gpt_oss.py
161+
```
162+
163+
Unit tests need no models. Integration tests need `LLM_MODELS_ROOT` **and** a
164+
GPU; forgetting the variable produces a failure that reads like a missing
165+
model rather than a missing setting.
166+
167+
### Multi-GPU: two mechanisms, and they are not interchangeable
168+
169+
**`mpi_pool_executor`** — a fixture, worker count by indirect parametrization:
170+
171+
```python
172+
@pytest.mark.parametrize("mpi_pool_executor", [2], indirect=True)
173+
def test_something(mpi_pool_executor): ...
174+
```
175+
176+
**A launcher in a fresh interpreter** — what the modeling_v2 collective
177+
entries use, and the reason is stated in
178+
`tests/unittest/_torch/modeling_v2/comm/_rank_job.py`:
179+
180+
> A collective that breaks \*hangs\* rather than raising, so the
181+
> deadline is load-bearing and the ``mpi_pool_executor`` fixture has none.
182+
183+
Two consequences worth carrying into any new multi-rank test:
184+
185+
- **A hang is the normal failure.** Whatever you write needs a deadline that
186+
kills the process group, or one wedged rank takes the whole run with it.
187+
- **You cannot start an MPI launcher from a pytest process that has already
188+
imported `tensorrt_llm`** — that process has initialized MPI. The launcher
189+
must go in a fresh interpreter, started *by file path*.
190+
191+
### Multi-node
192+
193+
The launch rules are the whole content here, and they come from
194+
`.claude/skills/trtllm-test-script-builder/`. **The rule is a function of the
195+
task count and nothing else:**
196+
197+
| | `ntasks == 1` | `ntasks > 1` |
198+
|---|---|---|
199+
| `--mpi=` | `none` | `pmix` (or the cluster's variant — ask `srun --mpi=list`) |
200+
| payload | clear the MPI environment, then exec | wrap in `trtllm-llmapi-launch` |
201+
202+
```bash
203+
srun -N 2 --ntasks 8 --ntasks-per-node 4 --mpi=pmix --gres=gpu:4 \
204+
--container-image=... --container-name=trtllm-run \
205+
bash -c "trtllm-llmapi-launch python3 -m tensorrt_llm.commands.serve $MODEL ..."
206+
```
207+
208+
Four things that are each a whole afternoon if you get them wrong:
209+
210+
1. **`--ntasks` must match the world size.** Without it `srun` launches one
211+
task, the MPI world size is 1, and the LLM API concludes it must create the
212+
other ranks itself. `need_spawn_mpi_workers` in `llmapi/mpi_session.py` is
213+
exactly `get_mpi_world_size() == 1 and model_world_size > 1`. Under pyxis
214+
that dynamic spawn is forbidden → `MPI_ERR_SPAWN`.
215+
2. **`--gres` is per node**, not per world. They diverge the moment you span
216+
nodes.
217+
3. **At one task, clear the MPI environment.** mpi4py auto-initializes MPI
218+
whenever PMI variables are present, and inside a PMIx-managed step they
219+
always are — so a single-task job initializes MPI for no reason and then
220+
dies spawning a worker. Keep `SLURM_PROCID` and `SLURM_LOCALID`; they
221+
identify the rank rather than bootstrap MPI.
222+
4. **`TLLM_WORKER_USE_SINGLE_PROCESS=1` is not the general fix.** The function
223+
behind it is named `enable_worker_single_process_for_tp1` and the source
224+
calls it a WAR for TP1. It does not scale past one rank.
225+
226+
Inside a multi-node compute session there is no `srun` and no sshd in the
227+
container. A session declared with `nodes: 2` runs one scheduler per node, and
228+
the workflow's `mn_mpirun.sh` (in the package's scripts directory, under
229+
`mn/`, with its own README) starts one `mpirun` across them, launching each remote daemon through
230+
that node's scheduler.
231+
232+
Inside a spool session the workflow emits the `mpirun` form of the same rule
233+
from `environment.py` (`mpirun -n <world> ... trtllm-llmapi-launch`); nothing
234+
here is an agent's decision. The `srun` form is written out for a human
235+
launching a test by hand, who needs to recognize a correct line.
236+
237+
## 4. Iterating without paying full price
238+
239+
| cost | how to stop paying it |
240+
|---|---|
241+
| container import, 26 GB / 4–8 min **per job** | `--container-name=<name>` — pyxis skips the import when that name already exists on the node |
242+
| queue wait per command | a persistent allocation: `salloc` once, then `srun --jobid=<id>` steps into it (see `exec-local-slurm`) |
243+
| both, plus a transport that cannot reach Slurm at all | a compute session — one long-lived job whose **own main process** is the spool scheduler; work is a file |
244+
| cold native build | `--use_ccache`, and one cache per checkout |
245+
246+
On the spool: work started inside a `slurm_exec_in_job` step does **not**
247+
survive the step's teardown, `nohup` included — measured. The long-running
248+
process has to be the job's own main process. If your cluster's Slurm CLI is
249+
reachable, prefer the persistent-allocation pattern; the spool exists for the
250+
case where it is not.
251+
252+
A session is declared in `task.yaml` (`environment.sessions`) and its entry
253+
script is rendered, never written by hand:
254+
255+
```bash
256+
trtllm-modeling-v2 session render --task task.yaml --name a # <spool>/session_entry.sh
257+
trtllm-modeling-v2 session submission --task task.yaml --name a # the broker call that starts it
258+
trtllm-modeling-v2 session status --task task.yaml # heartbeat, end, queue
259+
```
260+
261+
Inside a session, size each job instead of picking devices:
262+
263+
| you run | submit it as |
264+
|---|---|
265+
| a single-GPU unit test or microbenchmark | `--gpus 1` |
266+
| a 4-rank collective test | `--gpus 4` |
267+
| anything that loads the full model, a gate, a rebuild | `--gpus all` |
268+
| a worktree's code | add `--tree <worktree>` — sets the working directory **and** `PYTHONPATH`, and refuses a result whose ranks imported another checkout |
269+
| the orchestrator, or anything GPU-free and long-lived | a `free-` name, no `--gpus` |
270+
271+
`--wall` counts from the job's start, so time queued behind other work is
272+
free. `spoolctl.py status` says why each waiting job waits; `spoolctl.py cancel
273+
<name>` is the only way to stop one (see the safe-kill rule in
274+
TROUBLESHOOTING.md).
275+
276+
To abort one run without losing the allocation:
277+
278+
```bash
279+
squeue -s -j <job_id> # list steps
280+
scancel <job_id>.0 # cancel one step
281+
```
282+
283+
## 5. Where a new test belongs
284+
285+
1. **No device?** → a CPU list, and check whether a CBTS rule already narrows
286+
CI for your subtree (`jenkins/scripts/cbts/rules/`).
287+
2. **One device?** → the ordinary `l0_<gpu>.yml` for the architecture you
288+
certified on. Certification is per-architecture (`ARCHS`); a pass elsewhere
289+
is not a pass here.
290+
3. **Collectives or a TP/EP topology?** → `l0_*_multi_gpus.yml`, and give it a
291+
deadline.
292+
4. **Spans nodes?** → `l0_*_multi_nodes*.yml`, and it needs the launch rules
293+
in §3.
294+
295+
Then confirm with `scripts/test_to_stage_mapping.py` that it actually lands in
296+
a stage. A test on no list is a test nobody runs.
297+
298+
**One trap for documentation-shaped changes:** `ModelingV2Rule` deliberately
299+
excludes `*.md` so a documentation-only edit does not pull in multi-GPU GB300
300+
stages. That is the right trade, but it means a check that *scans* `.md` files
301+
gets no CI trigger from the PRs most likely to break it. Put text audits in a
302+
pre-commit hook, not in a GPU test stage.

0 commit comments

Comments
 (0)