feat(MODEL-MUSIC-MUSIC3): the queue its own comment named — a partial device arm, measured on Jetson Thor (#672) - #1004
Conversation
… device arm, measured on Jetson Thor (#672) FOLLOWING_AGENTS_PROTOCOL `minimax_music3_speech.cpp:492` built its `vt::Queue` from a compile-time constant CPU device, under a comment that already named the seam: "CPU is what W2 ships and what every gate for this row has been taken on; a device arm is a queue, not a fork." That was true and it was unreachable — nothing could supply a different queue, so a 28.5 GB music model was host-only whatever hardware the box had. It is now that queue. `multimodal::SpeechModelParams` grows `device` (0 = CPU, 1 = the accelerator this build resolves), the engine builds its queue once in the constructor before any weight is read, and `Music3LmSession` allocates its 36-layer paged KV on the queue's device — `dense_attn::KvSlice` labels that pointer with `d.q.device`, so a host `std::vector` handed to a CUDA forward is a host pointer wearing a device tensor's label. WHAT MOVES IS ONE OF SIX STAGES, and the commit says so because the alternative is a GPU claim that is really a CPU path with a flag. The 8.6B `Qwen3ForCausalLM` half runs on the accelerator through the shared `Qwen3DenseModel::ForwardEmbeds` that five text registrations already ride, so nothing was forked. The RVQ depth decoder and the fp32 DiT stay host scalar loops and are OWED. The DAC Flow-VAE vocoder is BLOCKED, not merely unfinished: `vt` has no `ConvTranspose1d` op at all, and `vt::Conv2d` / `vt::DepthwiseConv1d` are registered CPU-only, so the stage that is 88.5% of the acoustic half has no CUDA kernel behind any op it could route through. Hand-rolling one outside the seam is what AGENTS.md forbids. The device selector is a MAPPING, not a cast, and it went on the SEAM: the tree already carried two copies of the same three-question resolution (minimax_h3_video.cpp:255, ltx2_video.cpp:706) and a third is where copies start to disagree. Zero is CPU deliberately — not `vllm_model_params.device`'s 0=auto, which would move every zero-filled caller onto an ungated path on an accelerator build. MEASURED, Jetson Thor sm_110 in `vllmcpp-thor:cuda13.0.1`, both arms same box and same binary, flock-serialized on an idle box, one checkpoint resident at a time: correctness test_minimax_music3_llm_real, 25 teacher-forced steps vs frame_hiddens[:, :4096], AT THE BOUNDS THAT WERE ALREADY THERE: CPU 9337/102400 bit-identical, mean|d| 1.76348e-02, rank 2.48 CUDA 9324/102400 bit-identical, mean|d| 1.71668e-02, rank 2.44 control (torch MATH) 12036, 1.475e-02. Both 4 cases/220 assertions. The negative control fires identically on both (98.2% outside), so the bound still discriminates. The Thor CPU arm reproduces the recorded x86-64 numbers VALUE FOR VALUE. speed 2 AR frames 846.6 s CUDA vs 835.1 s CPU (1.014x SLOWER) 10 AR frames 1430.4 s CUDA vs 1512.1 s CPU (0.946x) fit on the difference: -11.65 s per AR frame, +34.8 s fixed, break-even ~3 frames. TWO POINTS DETERMINE A LINE EXACTLY, so that is an attribution with no residual, not a bound. No parity claim: SGLang-Omni is still `gateable = no` and every reference axis in BENCHMARKS stays PENDING. The two arms produce DIFFERENT SONGS by construction — spec §5 withdrew the token gate because the AR codes are a seeded multinomial draw — so no waveform comparison is offered, and the LM parity gate is the device arm's only numeric gate. It takes VLLM_CPP_MUSIC3_DEVICE, resolved through the SAME shared helper the engine calls so the gate cannot bind a different device than the engine. One instrument defect found and fixed inside this change: the gate's arm banner printed `ran on '1' (VLLM_CPP_MUSIC3_DEVICE=1)` on a CPU-only build with the variable unset — both fields collapsed inside doctest's MESSAGE chain. Had it not been read, the CPU arm's numbers would have been recorded as the device arm's. `test_capi` is RED on the Thor CUDA build and a matched-arm control built from `origin/main` c07526a in the same container reproduces it exactly: the recorded :487 SIGSEGV, plus two `structured_choice` failures that no baseline carried because the crash aborts the run before them. Filed as #994 rather than left behind a known-red name. C ABI v21: vllm_speech_model_params.device + vllm_speech_engine_device, which reports what was GRANTED rather than what was requested. `--speech-device` on the server, `--device` on minimax-music3-gen. The CPU arm takes the same host code, the same std::vector KV and the same queue the constant used to build, so it is bit-identical; every existing Music3 gate is unchanged. Issue: #672 Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
05c59f1 to
d3b27d4
Compare
CI, complete: 17 pass / 3 fail, and all three reds are attributed to a matched arm rather than to a label
Reproduced by running
Both were checked against a matched arm rather than read off a known-red list, because a known-red list tells you a job is often red, never that today's red is the same one — which is how the two FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true |
…ed by key `d9441ef3e` (#1004, the MiniMax-Music3 partial device arm) landed while this row was in review. It touches `include/vllm.h`, `src/capi/vllm_c.cpp` and the speech engine, none of which this row touches, but it also edits three keyed public records that this row edits. `docs/BENCHMARKS.md` conflicted. The conflict is adjacent-hunk context, NOT a same-key edit: `main` never changed the `LTX-2.5 axes` row, which is verified rather than assumed -- that row is byte-identical between the merge base and `origin/main`. Resolved by taking `origin/main`'s file wholesale and reapplying this row's single key, then proving the result differs from `origin/main` in exactly ONE line, and that line is the `LTX-2.5 axes` row. The other 508 lines are byte-identical. `docs/FEATURES.md` and `docs/USAGE.md` auto-merged, and the result was verified by key rather than accepted: FEATURES gains exactly one key (`LTX-2.5 Conv VAE decode threading`) with 0 removed and 0 changed; USAGE has 0 keys added, removed or changed, its edits being prose. `.agents/issue-index.md` is unchanged by this merge -- `#1004` appended no rows -- and `origin/main`'s file remains a byte-identical prefix at 271 rows, 271 unique ids. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
FOLLOWING_AGENTS_PROTOCOL
src/vllm/model_executor/models/minimax_music3_speech.cpp:492built itsvt::Queuefrom a compile-time constant CPU device, under a comment that already named the seam:That was true, and it was unreachable — nothing could supply a different queue, so a 28.5 GB music model was host-only whatever hardware the box had.
It is now that queue.
What moves, and what does not
One of six stages moves. Saying so is the point: the alternative is a GPU claim that is really a CPU path with a flag set.
--speech-device 1runs itQwen3ForCausalLM— prefill, every decode step, its paged KVThe language model reaches the device because it already rides the shared
Qwen3DenseModel::ForwardEmbedsthat five text registrations use; no model was forked. The vocoder is blocked on a missing op rather than on effort:vthas noConvTranspose1dof any kind, andvt::Conv2d/vt::DepthwiseConv1dare registered CPU-only (src/vt/cpu/cpu_conv2d.cpp:111,src/vt/cpu/cpu_conv1d_depthwise.cpp:95), so the stage that is 88.5 % of the acoustic half has no CUDA kernel behind any op it could route through. Hand-rolling one outside the seam is what AGENTS.md forbids, so it is recorded as owed (spec §11.4) — with three consumers (vocoder1dis shared with MiniMax-H3 and the LTX-2 audio VAE), that op is its own row.Three things this had to get right
static_cast<vt::DeviceType>(device)is the defectminimax_h3_video.cpp:230-237records. The tree already carried two copies of the correct three-question resolution (minimax_h3_video.cpp:255,ltx2_video.cpp:706); a third is where copies start to disagree, so this ismultimodal::SpeechEngineDeviceType, keyed on the family string.vllm_model_params.device's0=auto / 1=cpu / 2=cuda:autowould move every zero-filled caller — which is every caller written before this — onto an ungated path on an accelerator build.VideoModelParams::devicealready fixed this polarity for the sibling generative seam.dense_attn::KvSlice(dense_attn_block.h:233) labels the cache pointer withd.q.device, so 36 layers of hoststd::vector<uint16_t>on a CUDA queue is a host pointer wearing a device tensor's label. It is now allocated and zeroed on the queue's device.Two things did not need doing, and both were checked rather than assumed:
ResidentWeightstores its device copy on theOwnedTensoritself (w.d_dev), so weights upload once and are freed with the AR scope rather than cached against a reusable host address; andForwardEmbedsalready owns its own H2D/D2H.Measured — Jetson Thor sm_110, in the container
Host
kairos-4db2, aarch64, 14 cores, ~122 GB unified, driver 595.78, imagevllmcpp-thor:cuda13.0.1,--runtime=nvidia -e NVIDIA_DISABLE_REQUIRE=1,-DVLLM_CPP_CUDA=ON -DVLLM_CPP_CUDA_ARCHITECTURES=110 -DVLLM_CPP_TRITON=OFF, no cutlass. Checkpoint mounted read-only from the NAS. Every run heldflock $HOME/gpu.lock; one checkpoint resident at a time (this box reboots instead of OOM-killing);uptimerecorded on both sides of each run.Correctness — both arms, same box, same binary, SAME BOUNDS
tests/parity/test_minimax_music3_llm_real.cppnow takesVLLM_CPP_MUSIC3_DEVICE(default 0 = CPU, so an unset environment reproduces every number it ever printed), resolved through the same shared helper the engine calls so the gate cannot bind a different device than the engine. 25 teacher-forced steps vsframe_hiddens[:, :4096], 102 400 values:sdpa_kernel(MATH), recorded)Both 4 cases / 220 assertions / 0 failed, no tolerance widened. The CUDA arm is marginally closer to the golden and both sit inside the measured torch-vs-torch control. The negative control fires identically on both, so the bound still discriminates rather than having gone slack. And the Thor CPU arm reproduces the recorded x86-64 numbers value for value, so the CPU path is unchanged across two architectures, not merely on the box that measured it.
Speed — both arms, same request
--device 0--device 1--duration 0.1--duration 0.4Every other stage is the same host code on both arms and cancels in the difference:
Two points determine a line exactly, so that is an attribution with no residual and no error bar, stated as a fit rather than as a bound. The fixed cost is consistent with the one-time 17.2 GB
ResidentWeightupload plus context creation — not measured separately, and offered as the plausible reading.No parity claim. SGLang-Omni is still
gateable = no; every reference axis indocs/BENCHMARKS.mdstaysPENDING.Honest limits
torch.multinomialdraw, so a different logit changes the drawn code and everything after it. No waveform comparison is offered; the LM parity gate above is the device arm's only numeric gate.ran on '1' (VLLM_CPP_MUSIC3_DEVICE=1)on a CPU-only build with the variable unset — both fields collapsed inside doctest'sMESSAGEchain. Had it not been read, the CPU arm's numbers would have been recorded as the device arm's. Rebuilt as onestd::string.vt::MatmulBTwith device-resident weights;vt::ConvTranspose1dwith a CUDA provider for the vocoder; and the bit-identical row-wise parallelisation ofLinearNoBiasandvocoder1d::ConvTranspose1d, which needs its own measurement and is not in this change.Gates
Local x86-64 CPU build, all green with non-zero assertion counts (
assertions: 0is a skip wearing a pass):test_speech_engine11/38,test_capi65/653,test_minimax_music3_speech9/223,test_minimax_music3_loader21/1393 (21/1413 with the checkpoint),test_minimax_music3_ar25/338,test_minimax_music3_acoustic27/265,test_minimax_music3_quant29/125,test_minimax_music3_ar_real4/894,test_minimax_music3_acoustic_real6/76,test_minimax_music3_quant_real6/319,test_minimax_music3_llm_real4/220,test_minimax_h379/57395,test_indextts2_family7/22,test_openai_api_server62/733,test_speech_api6/67.Thor CUDA build:
test_speech_engine11/37 (one fewer BY DESIGN — the device-1 case takes its GRANTED branch there and its REFUSED branch on a CPU-only build, and prints which),test_minimax_music3_speech9/223,test_minimax_music3_llm_real4/220 on both arms.Known-red, checked against a matched arm rather than assumed:
test_capion the Thor CUDA build. A control built in the same container fromgit archive origin/main c07526aa1reproduces it exactly: the:487SIGSEGV already recorded indocs/STATUS.mdand in the Thor baseline sm_110 (Jetson Thor) ctest baseline at 2daa3287f: 15 red of 485, gated on (name, mode) pairs rather than a count #955, plus twostructured_choicefailures at:910/:932that no baseline carried, because the crash aborts the run before them. Main 62/60 passed/587 assertions/2 failed; this tree 64/62/622/2 — the delta is exactly the two cases and 35 assertions this PR adds. Filed as test_capi: two structured_choice cases fail on a CUDA build, hidden behind the :487 SIGSEGV (#955 baseline understates test_capi) #994 rather than left behind a known-red name.check-env-docREDs preflight on every branch on threeVT_MOE_EXPERT_STREAMknobs that landed in3005447f8; reproduced on pristineorigin/main. Already filed as check-env-doc REDs preflight on every branch: three VT_MOE_EXPERT_STREAM knobs landed undocumented in 3005447f8 #995 / main is red on check-env-doc: three VT_MOE_EXPERT_STREAM vars are neither documented nor allowlisted #1000, not this row's.windows-msvc-*is windows-msvc-cpu/vulkan red on every PR based on c7cb59fbb: C4244 double->float in ltx2_video.cpp's StreamState<->Ltx2LatentState positions copy (#964) #968 (an LTX-2.5C4244), untouched here.scripts/agent-preflight.sh --staged --no-require-roleexits 0 apart from the pre-existingcheck-env-docred above.Issue: #672
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]