Workload-conditioned operator profiling on patched vLLM 0.24.0 + Qwen3-30B-A3B/H20. H1b PASS (irregular patterns carry +23-45pp R64 raggedness, 8-45% token-efficiency loss vs rectangular controls); mechanism decomposition kills the padding narrative and finds the arrival-uniformization artifact (-12.9%); cross-version churn surface shows TP2/MNS64 -29.4% across vLLM 0.20->0.24 while the argmax held. Raw Layer-1 JSONL streams (507 MB) stay on disk, git-ignored; footer sidecars and metrics are tracked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
447 lines
26 KiB
Markdown
447 lines
26 KiB
Markdown
# Phase 0 recon: vLLM 0.24.0 observability
|
|
|
|
Status: source recon and patch design complete; **no instrumentation patch was
|
|
implemented**.
|
|
|
|
Campaign target: operator-level, pattern-conditioned profiling of
|
|
Qwen3-30B-A3B serving on NVIDIA H20. This report is based on the pinned source
|
|
clone only. No GPU was used, no package or model was installed, and no dash
|
|
host was contacted.
|
|
|
|
Unless explicitly prefixed with `v0.20.0:`, every source path and line number
|
|
below is relative to `/home/gahow/phd/vllm-v0.24.0` at the pinned commit.
|
|
|
|
## Outcome
|
|
|
|
vLLM 0.24.0 already contains most of the raw ingredients, but they are split
|
|
across scheduler internals, aggregate metrics, worker traces, and an optional
|
|
routed-expert return path. The smallest useful patch is therefore not a new
|
|
profiler. It is one scheduler-owned per-step record that joins existing batch
|
|
composition to the existing `CUDAGraphStat`; short torch-profiler windows and
|
|
exact expert routes remain sampled Layer 2 facilities.
|
|
|
|
The proposed patch is detailed in
|
|
[`patch-design.md`](patch-design.md). Estimated surface: five files and about
|
|
450 lines including tests, with about 260 production lines.
|
|
|
|
## 1. Source pin and compatibility record
|
|
|
|
The requested tag existed and the exact requested clone command completed
|
|
successfully:
|
|
|
|
```text
|
|
git clone --depth 1 --branch v0.24.0 https://github.com/vllm-project/vllm /home/gahow/phd/vllm-v0.24.0
|
|
```
|
|
|
|
| Field | Pinned value |
|
|
|---|---|
|
|
| Repository | `https://github.com/vllm-project/vllm` |
|
|
| Local clone | `/home/gahow/phd/vllm-v0.24.0` |
|
|
| Tag | `v0.24.0` (`git describe --tags --exact-match`) |
|
|
| Commit | `ee0da84ab9e04ac7610e28580af62c365e898389` |
|
|
| Clone completion | `2026-07-11T08:26:16Z` (`2026-07-11 16:26:16 +08:00`) |
|
|
| Checkout | Detached HEAD, clean |
|
|
| Top-level entries | 45, including hidden entries and `.git` |
|
|
| Listing SHA-256 | `75222e5d3bacdd043e421c417496aaf0fa5a9429b7244698b6d3ca2218b85b58` |
|
|
|
|
The listing digest is reproducible from entry names only, not file contents:
|
|
|
|
```text
|
|
LC_ALL=C find . -mindepth 1 -maxdepth 1 -printf '%f\n' |
|
|
LC_ALL=C sort | sha256sum
|
|
```
|
|
|
|
This definition includes `.git`, sorts bytewise under the C locale, and hashes
|
|
the 45 LF-terminated root names. It is recorded to make the otherwise ambiguous
|
|
phrase “sha256 of the top-level directory listing” precise.
|
|
|
|
### Torch and CUDA requirements
|
|
|
|
- Build and NVIDIA runtime requirements pin `torch==2.11.0` in
|
|
`pyproject.toml:1-13`, `requirements/build/cuda.txt:1-10`, and
|
|
`requirements/cuda.txt:1-10`; CMake repeats 2.11.0 as the supported CUDA and
|
|
ROCm torch version at `CMakeLists.txt:62-72`.
|
|
- The NVIDIA requirements also pin `torchvision==0.26.0`,
|
|
`flashinfer-python==0.6.12`, `flashinfer-cubin==0.6.12`, and CUDA-13 extras
|
|
for CUTLASS DSL and Humming kernels (`requirements/cuda.txt:6-29`). Setup
|
|
strips/substitutes those extras for CUDA 12 builds
|
|
(`setup.py:1066-1083`).
|
|
- The source default is CUDA 13.0 (`vllm/envs.py:85-87,555-563`) and the Docker
|
|
default is CUDA 13.0.2 (`docker/Dockerfile:14-42`). Wheel detection maps CUDA
|
|
major 12 to `cu129` and major 13 to `cu130` (`setup.py:528-568`).
|
|
- The pinned installation document still describes the default precompiled
|
|
binaries as CUDA 12.9 and lists CUDA 12.8 and 13.0 variants
|
|
(`docs/getting_started/installation/gpu.cuda.inc.md:4-19,24-54`). This is a
|
|
packaging/default distinction, not evidence that 13.0 is unsupported.
|
|
- NVIDIA compute capability 7.5+ is documented, and SM90 is in every relevant
|
|
CMake CUDA architecture set (`docs/getting_started/installation/gpu.cuda.inc.md:9-19`;
|
|
`CMakeLists.txt:105-118`). Optional FA3 and DeepGEMM builds need CUDA 12.3+,
|
|
FlashMLA needs 12.9+, and the Hopper CUTLASS MoE build needs 12.3+
|
|
(`setup.py:1111-1139`; `CMakeLists.txt:844-868`).
|
|
|
|
Implication for dash0 later: H20/SM90 is a supported architecture, but the
|
|
actual driver and installed CUDA were intentionally not queried in Phase 0.
|
|
Phase 2 must choose a torch 2.11.0-compatible `cu129` or `cu130` artifact after
|
|
checking the host driver. A wheel built against a different torch/CUDA build is
|
|
not interchangeable; the source documentation explicitly warns about binary
|
|
incompatibility (`docs/getting_started/installation/gpu.cuda.inc.md:14-19`).
|
|
|
|
## 2. Built-in observability inventory
|
|
|
|
### 2.1 Torch profiler integration
|
|
|
|
**Configuration and control.** v0.24.0 does not contain the legacy
|
|
`VLLM_TORCH_PROFILER_DIR` symbol: a repository-wide exact-name search at the
|
|
pinned commit returned zero matches. Profiling is now configured through
|
|
`ProfilerConfig`: `profiler` is `torch` or `cuda`, and
|
|
`torch_profiler_dir` holds the absolute/URI output location
|
|
(`vllm/config/profiler.py:33-46,125-145`). Optional controls include stacks,
|
|
FLOPs, gzip, CUDA-time tables, shapes, memory, frontend exclusion, delayed
|
|
start, maximum iterations, and wait/warmup/active iteration scheduling
|
|
(`vllm/config/profiler.py:48-105`).
|
|
|
|
The documented server form is a `--profiler-config` JSON object, for example
|
|
`{"profiler":"torch","torch_profiler_dir":"/abs/run/kineto"}`, followed by
|
|
POSTs to the two endpoints (`docs/contributing/profiling.md:46-85`).
|
|
|
|
When profiler config is present, the serving router exposes POST
|
|
`/start_profile` and `/stop_profile`; without profiler config the router is not
|
|
attached (`vllm/entrypoints/serve/profile/api_router.py:21-45`). The endpoints
|
|
are therefore not an unauthenticated always-on feature independent of server
|
|
configuration; they exist within the configured serving application and should
|
|
be protected like the rest of the control plane.
|
|
|
|
**Captured activity.** Each GPU worker creates a torch profiler with both CPU
|
|
and CUDA activities, using the configured shapes/memory/stack/FLOPs switches
|
|
(`vllm/v1/worker/gpu_worker.py:929-980` and
|
|
`vllm/profiler/wrapper.py:159-226`). TensorBoard-compatible traces are written
|
|
with an optional gzip handler. The wrapper also writes CPU/CUDA aggregate
|
|
tables; only rank 0 prints the table to stdout
|
|
(`vllm/profiler/wrapper.py:251-287`). The source documentation calls the
|
|
profiler medium-overhead and warns that trace size and final flushing can be
|
|
large (`docs/contributing/profiling.md:1-38`).
|
|
|
|
The V1 worker calls `profiler.step()` once per execution and wraps model
|
|
execution in a record-function name containing context/generation request and
|
|
token counts (`vllm/v1/worker/gpu_worker.py:803-827,895-898`). If frontend
|
|
profiling is not ignored, `AsyncLLM` also creates a separate CPU-only trace
|
|
(`vllm/v1/engine/async_llm.py:178-200`).
|
|
|
|
**TP behavior.** `EngineCore.profile()` delegates to the executor
|
|
(`vllm/v1/engine/core.py:662-663`), whose profile RPC is a collective worker
|
|
call (`vllm/v1/executor/abstract.py:256-257`). Multiprocessing broadcasts calls
|
|
that do not declare a unique output rank, so the profile call reaches every TP
|
|
worker (`vllm/v1/executor/multiproc_executor.py:340-402`). Each worker emits its
|
|
own rank-qualified trace; the suffix includes DP/PP/TP/DCP/EP/global rank
|
|
components (`vllm/distributed/utils.py:664-695`). For TP, kernel analysis must
|
|
therefore preserve per-rank files, use the slowest rank for critical-path time,
|
|
and optionally sum ranks to quantify total GPU work.
|
|
|
|
### 2.2 Iteration-level scheduler data and exported metrics
|
|
|
|
The V1 scheduler interface states that each `schedule()` output corresponds to
|
|
one model forward and maps request IDs to scheduled tokens
|
|
(`vllm/v1/core/sched/interface.py:51-80`). `SchedulerOutput` already carries
|
|
new/cached request data, per-request and total scheduled tokens, common-prefix
|
|
blocks, and the request IDs preempted in that step
|
|
(`vllm/v1/core/sched/output.py:180-219`). The base scheduler constructs this
|
|
object at `vllm/v1/core/sched/scheduler.py:1012-1076`, then advances request
|
|
computed-token and chunk state at `vllm/v1/core/sched/scheduler.py:1130-1155`.
|
|
|
|
What is available today:
|
|
|
|
| Requested datum | Internal per-step source | Externally exposed today |
|
|
|---|---|---|
|
|
| Batch size | `len(SchedulerOutput.num_scheduled_tokens)`; the map is exact | No raw per-step batch-size Prometheus series. Optional iteration log reports context and generation request counts. |
|
|
| Scheduled prefill/decode tokens | `compute_iteration_details()` classifies new/cached context requests and sums scheduled tokens (`vllm/v1/utils.py:766-813`) | Optional log reports exact scheduled context/generation counts and elapsed time around the execution wait (`vllm/v1/engine/core.py:433-472`). Prometheus prompt/generation counters are cumulative output-side stats, not a raw scheduled-step stream. |
|
|
| Chunked-prefill splits | `Request.is_prefill_chunk`, `num_computed_tokens`, and prompt/output state exist (`vllm/v1/request.py:130-175`); extended chunks are classified as context | No chunk ordinal, first/middle/final split, or chunk-size series. These must be derived before `_update_after_schedule()` mutates state. |
|
|
| Preemptions | Exact step set in `SchedulerOutput.preempted_req_ids` | Stdout aggregates over its logging interval; `vllm:num_preemptions` is cumulative (`vllm/v1/metrics/loggers.py:219-281,624-631`). |
|
|
| Running/waiting/deferred queues | `SchedulerStats.num_running_reqs`, `num_waiting_reqs`, and `num_skipped_waiting_reqs` (`vllm/v1/metrics/stats.py:170-183`) | Latest-value gauges `vllm:num_requests_running`, `vllm:num_requests_waiting`, and reason-labeled capacity/deferred gauges (`vllm/v1/metrics/loggers.py:453-496`). Running queue size is not the same as scheduled batch size. |
|
|
| KV-cache usage | Scheduler reads `kv_cache_manager.usage`; the block pool computes a null-block-adjusted ratio in `[0,1]` (`vllm/v1/core/kv_cache_manager.py:181-188`; `vllm/v1/core/block_pool.py:692-711`) | Latest-value `vllm:kv_cache_usage_perc`; stdout prints the latest percent (`vllm/v1/metrics/loggers.py:250-260,524-532`). Raw total/free blocks are not exported per step. |
|
|
| Prefix-cache counters | `PrefixCacheStats` contains request/query/hit and separately preempted request/query/hit counters (`vllm/v1/metrics/stats.py:114-143`) for local and optional connector caches | Prometheus exposes cumulative token queries/hits for local and external caches (`vllm/v1/metrics/loggers.py:547-565,1063-1101`); stdout reports interval-derived hit rates. It does not expose the preempted sub-counters separately. |
|
|
| Total step tokens | Exact scheduled total is in `SchedulerOutput` | `vllm:iteration_tokens_total` is a histogram observed from output-side computed prompt plus generation tokens (`vllm/v1/metrics/loggers.py:693-724,1148-1171`), not a joinable raw step record. |
|
|
|
|
`IterationStats` also has a wall timestamp, output-side prompt/generation data,
|
|
preemptions, and finished-request latency samples
|
|
(`vllm/v1/metrics/stats.py:325-347`). `Scheduler.make_stats()` produces latest
|
|
queue/KV state, drains prefix stats, and passes through the step's CUDA-graph
|
|
stat when an output is processed
|
|
(`vllm/v1/core/sched/scheduler.py:2228-2264`). These are valuable existing
|
|
plumbing, but async scheduling can schedule newer batches before an older
|
|
output is processed. Consequently, those latest/drained values are not a safe
|
|
per-step composition join without a schedule-time snapshot.
|
|
|
|
Bottom line: no new scheduler instrumentation is needed to discover batch and
|
|
token composition, preemptions, queues, KV use, or prefix events. A patch is
|
|
needed only to serialize their **same-step association** and the missing
|
|
histograms/chunk split.
|
|
|
|
### 2.3 CUDA-graph observability
|
|
|
|
Yes, v0.24.0 can determine the runtime graph mode and capture bucket for each
|
|
model step internally:
|
|
|
|
- `CUDAGraphStat` records unpadded tokens, padded tokens, padding, and runtime
|
|
mode (`vllm/compilation/cuda_graph.py:32-37`).
|
|
- The GPU model runner asks the dispatcher for a concrete runtime mode and
|
|
`BatchDescriptor`, then conditionally attaches the stat to its output
|
|
(`vllm/v1/worker/gpu_model_runner.py:3822-3934`). It reaches the scheduler in
|
|
`ModelRunnerOutput` and `update_from_output()`
|
|
(`vllm/v1/outputs.py:231-281`;
|
|
`vllm/v1/core/sched/scheduler.py:1464-1477`).
|
|
- The dispatcher is the source of truth: it pads to a captured descriptor,
|
|
tries `FULL`, then `PIECEWISE`, and returns `NONE` when no graph key matches
|
|
(`vllm/v1/cudagraph_dispatcher.py:235-324`; the design explanation is at
|
|
`docs/design/cuda_graphs.md:81-120`). At serving time, `FULL` and `PIECEWISE`
|
|
dispatch to a captured wrapper; `NONE` calls the runnable directly
|
|
(`vllm/compilation/cuda_graph.py:233-360`).
|
|
|
|
Thus `runtime_mode != NONE` is a graph-path dispatch, and `num_padded_tokens` is
|
|
the selected token bucket. `PIECEWISE` means graph-wrapped compiled pieces, not
|
|
a full-step graph hit. Startup normally captures all dispatcher descriptors
|
|
(`vllm/v1/worker/gpu_model_runner.py:6595-6643`), but `CUDAGraphStat` has no
|
|
capture-versus-replay bit; a lazy first capture and a replay have the same stat.
|
|
With built-in `cudagraph_metrics`, the stat is aggregated
|
|
into a frequency table and printed at the logging interval
|
|
(`vllm/config/observability.py:56-58`;
|
|
`vllm/compilation/cuda_graph.py:40-124`); it is not exposed as a raw per-step
|
|
Prometheus event. OpProf only needs to preserve the already-computed object on
|
|
every enabled step.
|
|
|
|
Capture modes and sizes live in `CompilationConfig`:
|
|
`cudagraph_mode`, `cudagraph_capture_sizes`, and
|
|
`max_cudagraph_capture_size` are defined at
|
|
`vllm/config/compilation.py:587-688`. The V1 default is
|
|
`FULL_AND_PIECEWISE`. Final sizes are generated or validated in
|
|
`VllmConfig._set_cudagraph_sizes()`; the default is 1/2/4, then steps of 8 below
|
|
256 and 16 above it, bounded by token/batch configuration, with user overrides
|
|
written back to the final list (`vllm/config/vllm.py:1635-1800`).
|
|
|
|
### 2.4 MoE routing and Qwen3-30B-A3B backend
|
|
|
|
Qwen3 MoE creates a replicated gate and `FusedMoE` with the model's number of
|
|
experts and top-k, and normally runs its router internally
|
|
(`vllm/model_executor/models/qwen3_moe.py:137-249`). The causal-LM wrapper
|
|
retains the list of MoE layers and their expert topology
|
|
(`vllm/model_executor/models/qwen3_moe.py:662-729`). Router top-k IDs are
|
|
available immediately after routing and before EPLB remapping
|
|
(`vllm/model_executor/layers/fused_moe/router/base_router.py:233-278`).
|
|
|
|
There is an exact, built-in access path: `--enable-return-routed-experts`
|
|
(`vllm/config/model.py:209-215`; `vllm/engine/arg_utils.py:826-830`). It installs
|
|
per-layer callbacks, writes top-k IDs to preallocated GPU buffers, copies the
|
|
step to pinned CPU memory, and returns it through `ModelRunnerOutput`
|
|
(`vllm/model_executor/layers/fused_moe/routed_experts_capturer.py:58-84`;
|
|
`vllm/v1/worker/gpu_model_runner.py:3652-3666,4637-4683,7382-7437`). This is
|
|
reachable without modifying the fused kernels or saving router logits.
|
|
Raw router-logit tensors are not plumbed to engine outputs; exposing those
|
|
would be deeper and much higher-volume surgery than using the existing top-k
|
|
ID callback.
|
|
|
|
It is not cheap enough for Layer 1. The per-worker transit buffer is a few MB;
|
|
the scheduler's whole-slot buffer can reach multiple GB and performs a CPU
|
|
fancy-index write each step
|
|
(`vllm/model_executor/layers/fused_moe/routed_experts_capturer.py:223-305`;
|
|
the scheduler store is at `vllm/v1/core/sched/scheduler.py:1501-1520`). It is
|
|
also incompatible with pipeline parallelism greater than one and with KV
|
|
connectors (`vllm/config/vllm.py:871-893`). Exact per-layer expert load should
|
|
therefore be collected in a separate sampled Layer-2 run and reduced offline.
|
|
EPLB has load state, but enabling it adds communication and its logs are
|
|
aggregate balancing summaries; it is not a free observability tap
|
|
(`vllm/distributed/eplb/eplb_state.py:62-171,478-575`).
|
|
|
|
For the normal unquantized BF16/FP16 Qwen3-30B-A3B configuration on H20/SM90,
|
|
the automatic unquantized oracle explicitly moves both FlashInfer backends
|
|
behind Triton on Hopper because they are expected to be slower
|
|
(`vllm/model_executor/layers/fused_moe/oracle/unquantized.py:43-86`). It selects
|
|
the first supported backend and documents possible shape-specific fallbacks
|
|
(`vllm/model_executor/layers/fused_moe/oracle/unquantized.py:152-250`). The
|
|
primary backend is therefore **Triton** under those assumptions. A quantized
|
|
checkpoint, LoRA, expert activation format, or explicit `--moe-backend` can
|
|
change that conclusion and must be recorded with the campaign run.
|
|
|
|
### 2.5 V1 execution loop and natural hooks
|
|
|
|
The control path is:
|
|
|
|
```text
|
|
EngineCore busy loop
|
|
-> Scheduler.schedule()
|
|
-> executor.execute_model(SchedulerOutput)
|
|
-> GPUWorker -> GPUModelRunner.execute_model()
|
|
-> Scheduler.update_from_output(SchedulerOutput, ModelRunnerOutput)
|
|
```
|
|
|
|
Evidence and hook implications:
|
|
|
|
- `EngineCore.run_busy_loop()` and `_process_engine_step()` drive steps at
|
|
`vllm/v1/engine/core.py:1259-1317`.
|
|
- The normal step retains the exact scheduler output across the future and
|
|
passes it back to `update_from_output()`
|
|
(`vllm/v1/engine/core.py:479-508`). The batch-queue path does the same for
|
|
multiple in-flight batches (`vllm/v1/engine/core.py:519-632`).
|
|
- The scheduler's stable interface is schedule/update at
|
|
`vllm/v1/core/sched/interface.py:51-107`. The base implementation builds the
|
|
output at `vllm/v1/core/sched/scheduler.py:1012-1100`, mutates request state
|
|
immediately afterward at lines 1130-1155, and consumes model results at
|
|
lines 1464-1803.
|
|
- Worker execution wraps `gpu_model_runner.execute_model()` at
|
|
`vllm/v1/worker/gpu_worker.py:895-898`; the model runner's entry is
|
|
`vllm/v1/worker/gpu_model_runner.py:4055-4069`.
|
|
|
|
The natural Layer-1 hooks are therefore in the base scheduler: snapshot after
|
|
the `SchedulerOutput` is complete but before request state advances, then
|
|
finalize with the matching model output. This covers the async subclass and
|
|
avoids frontend/output aggregation. The only worker change is broadening the
|
|
existing CUDA-graph-stat condition. Layer 2 uses the existing profile control
|
|
path unchanged.
|
|
|
|
### 2.6 Relevant changes from 0.20.0
|
|
|
|
For comparison only, tag `v0.20.0` was fetched into the allowed clone without
|
|
changing the pinned working tree. It resolves to commit
|
|
`88d34c6409e9fb3c7b8ca0c04756f061d2099eb1`.
|
|
|
|
- Async scheduling and the batch-queue architecture already existed in 0.20.0;
|
|
`SchedulerConfig` selected `AsyncScheduler` at
|
|
`v0.20.0:vllm/config/scheduler.py:146-176`, and the core schedule/future/update
|
|
paths were at `v0.20.0:vllm/v1/engine/core.py:402-431,443-533`. It is
|
|
incorrect to treat async scheduling as new in 0.24.0.
|
|
- 0.24.0 adds DP prefill cadence/throttling and passes a prefill-throttle flag
|
|
into `schedule()` (`vllm/config/scheduler.py:140-161` and
|
|
`vllm/v1/engine/core.py:474-490`). It also hardens zero-token/dummy-iteration
|
|
handling (`vllm/v1/engine/core.py:433-472`) and changes when the batch queue
|
|
fills.
|
|
- The 0.24 scheduler contains explicit scheduled/processed sequence fences and
|
|
snapshots state that must survive later async schedules, including routed
|
|
experts (`vllm/v1/core/sched/scheduler.py:293-322,1093-1165`). That is direct
|
|
evidence that an OpProf hook must not read mutable request/queue state only
|
|
when an older output returns.
|
|
- `vllm/config/profiler.py`, `vllm/profiler/wrapper.py`, and
|
|
`vllm/entrypoints/serve/profile/api_router.py` are byte-identical between the
|
|
two tags (`git diff --quiet v0.20.0..v0.24.0` returned success). Profiler
|
|
endpoint semantics did not move; the scheduler/core line numbers and safe
|
|
composition hook did.
|
|
|
|
Practical conclusion: do not port a 0.20 line-number patch into EngineCore.
|
|
Hook the 0.24 base scheduler's schedule/update pair so sync, async, and batch
|
|
queue paths share one implementation.
|
|
|
|
## 3. Proposed dual-layer design
|
|
|
|
Layer 1 emits one schema-versioned JSONL record per scheduler step from the
|
|
EngineCore/scheduler process. It contains step/timestamps; scheduled and decode
|
|
batch counts; scheduled prefill/decode tokens; aggregate first/middle/final
|
|
chunk information; fixed-bucket context and chunk-size histograms; exact step
|
|
preemptions; schedule-time running/waiting/deferred queues; total/free/used KV
|
|
blocks and ratio; local/external prefix deltas; and CUDA-graph mode/bucket. No
|
|
request IDs or raw context-length lists are written. MoE expert load is null and
|
|
explicitly marked Layer-2-only.
|
|
|
|
Records are encoded with the existing `msgspec` dependency
|
|
(`requirements/common.txt:35`), placed into a
|
|
bounded non-blocking queue, and drained by one background JSONL writer. It
|
|
flushes every 1 MiB or one second and at clean shutdown, with explicit drop/gap
|
|
counters and no per-step `fsync`. `VLLM_OPPROF_DIR` is the startup-only switch;
|
|
unset is a true no-op. The unmeasured foreground estimate is 25-100
|
|
microseconds per step, so Phase 2 must enforce the less-than-3% overhead gate
|
|
with paired off/on serving runs.
|
|
|
|
Layer 2 configures the already-built torch profiler and samples short windows,
|
|
initially two warm-up plus eight active worker iterations. Set
|
|
`max_iterations=8`; the wrapper stops the underlying profiler on the subsequent
|
|
step after the counter exceeds the limit, as its existing test documents
|
|
(`vllm/profiler/wrapper.py:83-114`;
|
|
`tests/v1/worker/test_gpu_profiler.py:77-98`). The controller then calls
|
|
`/stop_profile` to clear control state. Step-count/on-demand endpoint control is
|
|
preferred to wall time. Layer 2 writes one trace per TP worker and joins to
|
|
Layer 1 through the existing iteration annotation, order, token counts, and
|
|
timestamps.
|
|
|
|
Under CUDA-graph replay, the profiler is configured for CUDA activity, but the
|
|
Python callable is not re-executed: vLLM calls `CUDAGraph.replay()`
|
|
(`vllm/compilation/cuda_graph.py:233-360`). CUDA activity may therefore be
|
|
visible through Kineto/CUPTI, while Python/PyTorch scopes cannot be assumed to
|
|
provide the original per-layer attribution. Layerwise NVTX is explicitly
|
|
unsupported with graphs (`vllm/config/observability.py:60-63`). Phase 2 must
|
|
check whether the pinned torch/CUPTI combination exposes individual replayed
|
|
kernels or a coarser graph launch. A matched eager window can identify kernel
|
|
families but must not replace graph-enabled production timing.
|
|
|
|
Layer-2 kernel time is grouped into attention, router/top-k, expert/dense GEMMs,
|
|
normalization/activation, collectives, and sampling. Per-rank critical-path
|
|
times calibrate a composition/graph-bucket-conditioned Layer-1 attribution;
|
|
held-out profiler windows validate that mapping. Exact routed experts are
|
|
enabled only in a separate sampled server run.
|
|
|
|
### Patch surface
|
|
|
|
| File | Approximate lines |
|
|
|---|---:|
|
|
| `vllm/envs.py` | 4 |
|
|
| `vllm/v1/opprof.py` (new) | 220 |
|
|
| `vllm/v1/core/sched/scheduler.py` | 32 |
|
|
| `vllm/v1/worker/gpu_model_runner.py` | 4 |
|
|
| `tests/v1/core/test_opprof.py` (new) | 190 |
|
|
| **Total** | **5 files / about 450 lines** |
|
|
|
|
Zero-patch wins: profiler config/endpoints/wrapper, `SchedulerOutput`, existing
|
|
metrics plumbing, CUDA kernels, fused MoE/router code, Qwen3 model code, and
|
|
frontend APIs. Supporting OpProf together with `--disable-log-stats` would add
|
|
about 12 lines in `vllm/v1/core/kv_cache_manager.py`; the design recommends
|
|
failing fast instead.
|
|
|
|
### Main risks
|
|
|
|
- Histogram construction and JSON encoding are the likely foreground overhead;
|
|
the 3% budget must be measured, not inferred.
|
|
- Async scheduling can misassociate latest queue/cache state with an older
|
|
output; snapshot inside `schedule()` and pair by the exact output object.
|
|
- Layer 1 must be scheduler-owned to avoid TP duplicates; Layer 2 remains
|
|
per-rank and needs offline aggregation.
|
|
- At 500 steps/s and about 1 KiB/record, an uncompressed stream is roughly
|
|
44 GB/day per EngineCore. Runs must be duration-bounded, with queue/drop
|
|
counters and post-run compression.
|
|
- Kineto perturbs execution, and graph replay loses Python-scope attribution;
|
|
it is a sampled calibration instrument, not an always-on latency source.
|
|
|
|
## 4. Open design decisions
|
|
|
|
1. Approve JSONL/`msgspec`, an 8192-record queue, context edges
|
|
`(128, 256, 512, 1K, 2K, 4K, 8K, 16K, 32K, 64K, 128K, +inf)`, and chunk-size
|
|
edges `(16, 32, 64, 128, 256, 512, 1K, 2K, +inf)`.
|
|
2. Approve exact MoE expert load as Layer-2-only. The alternative is a new GPU
|
|
histogram path, with a larger patch and an overhead/graph-capture risk.
|
|
3. Confirm checkpoint precision/quantization and intended TP/EP/DP topology.
|
|
The H20 Triton conclusion assumes unquantized BF16/FP16, auto backend, and no
|
|
LoRA.
|
|
4. Choose the initial profiler window: recommended two warm-up plus eight
|
|
active iterations, or a longer active window.
|
|
5. Decide whether the less-than-3% gate applies to point estimates or the upper
|
|
95% confidence bound for every primary metric.
|
|
6. Confirm that OpProf campaign runs may reject `--disable-log-stats`; support
|
|
for it increases the patch to six files and about 462 lines.
|
|
|
|
## Data sanity block
|
|
|
|
- **Pinned root listing:** n=45 names; lexicographic min=`.buildkite`,
|
|
max=`vllm`; distinct=45; SHA-256=`75222e5d3bacdd043e421c417496aaf0fa5a9429b7244698b6d3ca2218b85b58`.
|
|
- **Source versions compared:** n=2 tags; min=`v0.20.0`, max=`v0.24.0`;
|
|
distinct=2; pinned working-tree version remains exactly `v0.24.0`.
|
|
- **Torch pin observations:** n=4 declarations
|
|
(`pyproject`, build requirements, CUDA runtime requirements, CMake);
|
|
min=max=`2.11.0`; distinct=1.
|
|
- **Evidence-bearing source files inspected and cited:** n=44
|
|
distinct v0.24.0 paths; min/max not applicable to categorical paths;
|
|
v0.20.0 comparison touched n=5 distinct paths; min/max not applicable.
|
|
- **Invariants checked:** exact tag equals `v0.24.0`; HEAD equals the recorded
|
|
commit; clone is clean; top-level names are unique; requested path was used;
|
|
torch pins agree; CUDA-graph modes are in `{NONE, PIECEWISE, FULL}`; KV usage
|
|
definition is bounded in `[0,1]`; scheduled-token total is defined as the sum
|
|
of per-request scheduled tokens; graph bucket is not smaller than unpadded
|
|
tokens; no remote/GPU/install/model-download action occurred.
|
|
- **Benchmark/statistical conclusion check:** no performance benchmark was run,
|
|
so there are no per-configuration curves or measured overhead values on which
|
|
to perform identical-value, monotonicity, or continuity checks. All overhead
|
|
numbers in the design are labeled estimates.
|