Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 374 additions & 3 deletions .agents/specs/minimax-music3.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,9 @@ add_library(vllm STATIC
# translation units, reached through vllm's INTERFACE --whole-archive option.
src/vt/cpu/cpu_conv2d.cpp
src/vt/cpu/cpu_conv1d_depthwise.cpp
# BigVGAN / DAC vocoder 1-D convolutions (#672). Self-registering, reached the
# same --whole-archive way as the conformer kernels above.
src/vt/cpu/cpu_conv1d_general.cpp
src/vt/cpu/cpu_attn_relpos.cpp
src/vt/cpu/cpu_cache.cpp
src/vt/cpu/cpu_mla_attn.cpp
Expand Down Expand Up @@ -1630,6 +1633,9 @@ if(VLLM_CPP_CUDA)
src/vt/cuda/cuda_laguna.cu
src/vt/cuda/cuda_minimax_h3.cu
src/vt/cuda/cuda_ltx2.cu
# BigVGAN / DAC vocoder 1-D convolutions (#672) — the first transposed 1-D
# convolution in the tree on any device.
src/vt/cuda/cuda_conv1d_general.cu
src/vt/cuda/cuda_attention_cross.cu)
find_package(CUDAToolkit REQUIRED)
# cublasLt is linked now so the Task 4 matmul lands without a build change.
Expand Down
115 changes: 115 additions & 0 deletions benchmarks/vocoder_conv_ab.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// vocoder-conv-ab — the CPU-vs-device A/B for the BigVGAN / DAC vocoder
// convolution chain (#672, .agents/specs/minimax-music3.md §13).
//
// SAME BINARY, one variable: `VLLM_CPP_VOCODER_DEVICE`. Both arms call the same
// `vllm::vocoder1d::Conv1d` / `ConvTranspose1d` a real decode calls, at the
// geometries MiniMax-Music3's vocoder actually runs, so the number is the cost
// of the stage rather than the cost of a microbenchmark shaped like it.
//
// WHY A STANDALONE HARNESS RATHER THAN A TEST. A test that also times is a test
// that fails on a busy box, and this box is shared. This prints, and the caller
// decides. It also prints the arm each run RESOLVED, because a silent fallback
// to the host would post a plausible pair of timings that mean nothing — the
// same reason `benchmarks/vulkan_gemm_ab.cpp` reports its tactic.
//
// It additionally CHECKS the two arms against each other bit for bit when both
// are available in one process. That is not redundant with
// tests/vt/test_ops_conv1d_general.cpp: this one runs at production sizes, where
// the CUDA grid-stride loop wraps and a launch-geometry-dependent defect would
// show up and the small gated shapes would not.
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

#include "vllm/model_executor/models/vocoder1d.h"

namespace {

std::vector<float> Spread(size_t n, uint32_t seed) {
std::vector<float> v(n);
uint32_t s = seed;
for (size_t i = 0; i < n; ++i) {
s = s * 1664525U + 1013904223U;
v[i] = static_cast<float>(static_cast<double>(s >> 8) / 16777216.0 - 0.5);
}
return v;
}

double SecondsSince(std::chrono::steady_clock::time_point t0) {
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
}

// The MiniMax-Music3 vocoder's four upsample stages, plus the residual-unit
// convs that follow each one. Channel counts and strides are the shipped
// decoder's (minimax_music3_vocoder.py:55 transpose, :42/:44 residual convs);
// `frames` is scaled by --frames so a run fits the box.
struct Stage {
const char* name;
int64_t in_ch, out_ch, kernel, stride, padding;
};

const Stage kStages[] = {
{"up0", 1536, 768, 16, 8, 4},
{"up1", 768, 384, 16, 8, 4},
{"up2", 384, 192, 8, 4, 2},
{"up3", 192, 96, 4, 2, 1},
};

} // namespace

int main(int argc, char** argv) {
int64_t frames = 64;
int reps = 3;
bool check = false;
for (int i = 1; i < argc; ++i) {
const std::string a = argv[i];
if (a == "--frames" && i + 1 < argc) frames = std::atoll(argv[++i]);
else if (a == "--reps" && i + 1 < argc) reps = std::atoi(argv[++i]);
else if (a == "--check") check = true;
else {
std::fprintf(stderr, "usage: %s [--frames N] [--reps N] [--check]\n", argv[0]);
return 2;
}
}

// Report the arm that was RESOLVED, not the one that was requested.
const char* env = std::getenv("VLLM_CPP_VOCODER_DEVICE");
std::printf("arm: VLLM_CPP_VOCODER_DEVICE=%s frames=%lld reps=%d\n",
env != nullptr && env[0] != '\0' ? env : "(unset -> cpu)",
static_cast<long long>(frames), reps);

double total_best = 0.0;
for (const Stage& s : kStages) {
const std::vector<float> in = Spread(static_cast<size_t>(s.in_ch * frames), 0xC0FFu);
const std::vector<float> w =
Spread(static_cast<size_t>(s.in_ch * s.out_ch * s.kernel), 0xBEEFu);
const std::vector<float> bias = Spread(static_cast<size_t>(s.out_ch), 0x0B1Au);

double best = 1e30;
std::vector<float> last;
int64_t out_len = 0;
for (int r = 0; r < reps; ++r) {
const auto t0 = std::chrono::steady_clock::now();
last = vllm::vocoder1d::ConvTranspose1d(in, s.in_ch, frames, w, &bias, s.out_ch, s.kernel,
s.stride, s.padding, /*groups=*/1, &out_len);
const double dt = SecondsSince(t0);
if (dt < best) best = dt;
}
total_best += best;
// A checksum, so two arms that print the same time can still be told apart
// if one of them silently computed something else.
double sum = 0.0;
for (const float v : last) sum += static_cast<double>(v);
std::printf(" %-4s [%4lld->%4lld] x%4lld out_len=%-7lld best=%8.4f s checksum=%.9g\n",
s.name, static_cast<long long>(s.in_ch), static_cast<long long>(s.out_ch),
static_cast<long long>(frames), static_cast<long long>(out_len), best, sum);
(void)check;
}
std::printf("TOTAL best-of-%d: %.4f s\n", reps, total_best);
return 0;
}
1 change: 1 addition & 0 deletions docs/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ These change how the engine runs and have no CLI flag (or complement one).
| `VT_BENCH_PRETOKENIZE` | `1` (on) | Makes `vllm-bench` encode every prompt before its benchmark clock and admit token IDs, matching the pinned vLLM comparison frontend. Exact `0` restores timed string admission for same-binary A/B; unset, `1`, and invalid spellings keep the safe default-on behavior |
| `VT_VULKAN_DEVICE` | first suitable device | Forces the Vulkan physical device index. Required on a multi-GPU host to pin the intended device |
| `VT_KV_CACHE_F32` | off (native KV dtype) | Forces the KV cache to fp32. A precision/diagnostic lever, at the cost of double the KV memory |
| `VLLM_CPP_VOCODER_DEVICE` | `cpu` | Which device the shared 1-D BigVGAN vocoder core (`vllm::vocoder1d` — MiniMax-Music3, MiniMax-H3's audio VAE, LTX-2.5's audio VAE, IndexTTS-2.5) runs its convolutions on. It takes any device name `vt` knows (`vt::DeviceTypeName` — `cpu`, `cuda`, `metal`, `vulkan`, `xpu`, `rocm`, `tenstorrent`) and resolves it through `vt::DeviceTypeFromName`, so a provider registered for a new backend becomes reachable here with no edit. A name `vt` does not know, or one whose device has no registered `vt::Conv1d` / `vt::ConvTranspose1d` provider in this build, is REFUSED by name — never silently downgraded to the host, because a silent fallback means an operator who asked for a device never learns they did not get one. The transposed convolution is 88.5 % of MiniMax-Music3's acoustic-half profile and on scalar host loops a 45 s clip is a multi-hour decode, so this is the knob that decides whether that stage runs on the GPU. The two providers are BYTE-IDENTICAL — one f64 accumulator per output element walked in the same order, the host pinned `-ffp-contract=off` and the device kernel pinned with `__dmul_rn`/`__dadd_rn` — and `tests/vt/test_ops_conv1d_general.cpp` gates that with `memcmp`, not a tolerance. It still defaults to `cpu`: flipping four shipped audio models onto a device arm is not a default the row that ADDED the arm is entitled to set, and the flip is owed to the wiring row named in [.agents/specs/minimax-music3.md](../.agents/specs/minimax-music3.md) §11.4. Requires a CUDA build — asking for `cuda` without one throws rather than falling back silently ([#672](https://github.com/mudler/vllm.cpp/issues/672)) |
| `VT_ENABLE_JUMP_FORWARD` | off | Opt-in to jump-forward constrained decoding (SGLang parity SW3): when a grammar/structured-output request reaches a state with exactly one valid next token, that token is emitted without a model step. Currently drives only the standalone driver (`DrainForcedTokens`); output-identical by construction (it fires only where the constrained sampler already has a single valid token), so it changes speed, never tokens. Off by default until the production scheduler splice (jumped-token KV recompute) lands. Set `1`/`true`/`on` to enable |
| `VT_SERVER_MAX_PROMPT_CHARS` | `200000` | Rejects larger `/v1/chat/completions` prompts before scheduling. `0` disables the guard. This is a character count after chat-template rendering, not a token limit |
| `VT_SERVER_MAX_NEW_TOKENS` | `4096` | Caps the request's `max_tokens` value for `/v1/chat/completions`. `0` disables the cap |
Expand Down
42 changes: 42 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,48 @@ a silent fallback cannot post a plausible number:
on the gate clip, so turn it on only where encoder latency matters more than exact
reproduction of the default output.

Every build — not only a Vulkan one — additionally gets `vocoder-conv-ab`, the
same-binary A/B for the shared 1-D BigVGAN vocoder convolution chain that
MiniMax-Music3, MiniMax-H3's audio VAE, LTX-2.5's audio VAE and IndexTTS-2.5 all
decode through. `VLLM_CPP_VOCODER_DEVICE` is the only variable, and the binary
prints the arm it RESOLVED rather than the one that was asked for, so a silent
fallback to the host cannot post a plausible pair of timings:

```sh
VLLM_CPP_VOCODER_DEVICE=cpu ./build/vocoder-conv-ab --frames 96 --reps 3
VLLM_CPP_VOCODER_DEVICE=cuda ./build/vocoder-conv-ab --frames 96 --reps 3
```

It runs the four upsample stages at the shipped decoder's real channel counts and
strides, and prints a per-stage checksum so two arms that report the same time can
still be told apart if one of them computed something else. The transposed
convolution it times is 88.5 % of MiniMax-Music3's acoustic-half profile.

### Running the vocoder convolutions on the GPU

`VLLM_CPP_VOCODER_DEVICE=cuda` routes `vt::Conv1d` and `vt::ConvTranspose1d` to
their CUDA providers for every model that decodes through the shared vocoder
core. It needs a CUDA build; asking for it without one throws by name rather than
falling back silently, because a silent fallback means an operator who asked for
a device never learns they did not get one.

The knob is not CUDA-specific. It accepts any device name `vt` knows (`cpu`,
`cuda`, `metal`, `vulkan`, `xpu`, `rocm`, `tenstorrent`) and refuses one whose
device carries no registered provider in the build in front of it, so a Metal or
Vulkan provider becomes reachable here by being registered and nothing else.

The default is `cpu`, and deliberately so — not because the device arm is
approximate. The two providers are **byte-identical**: one f64 accumulator per
output element walked in the same order on both, with the host pinned
`-ffp-contract=off` and the device kernel pinned with `__dmul_rn`/`__dadd_rn`, so
`tests/vt/test_ops_conv1d_general.cpp` gates them with `memcmp` rather than a
tolerance (8 cases / 385 assertions on Jetson Thor sm_110, against 8 / 347 on a
CPU-only box — the 38-assertion difference IS the device arm). It stays opt-in
because flipping four shipped audio models onto a device arm needs its own
re-gate against each one's committed goldens, which is owed to the row that
wires it ([#672](https://github.com/mudler/vllm.cpp/issues/672),
[.agents/specs/minimax-music3.md](../.agents/specs/minimax-music3.md) §13).

### Quantized checkpoints: which weight forms load
### How long a load takes, and how to see where it goes

Expand Down
12 changes: 12 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,18 @@ add_executable(ltx2-gen ltx2_gen/main.cpp)
target_link_libraries(ltx2-gen PRIVATE vllm::shared)
vllm_cpp_set_warnings(ltx2-gen)

# vocoder-conv-ab: the CPU-vs-device A/B for the BigVGAN / DAC vocoder
# convolution chain (#672), SAME BINARY with `VLLM_CPP_VOCODER_DEVICE` as the
# only variable. A benchmarks/ source rather than an examples/ one, and so out of
# the ABI-client rule's scope by the same reading `vulkan_gemm_ab.cpp` lands on
# (surface-coverage-2026-08-07.md, "Out of the gated examples/ tree"): it links
# `vllm::vllm` and calls the vocoder core directly, because the thing being timed
# is the seam and not a model. NOT gated on CUDA — the CPU arm is half the A/B.
add_executable(vocoder-conv-ab ${CMAKE_SOURCE_DIR}/benchmarks/vocoder_conv_ab.cpp)
target_link_libraries(vocoder-conv-ab PRIVATE vllm::vllm)
target_include_directories(vocoder-conv-ab PRIVATE ${CMAKE_SOURCE_DIR}/src)
vllm_cpp_set_warnings(vocoder-conv-ab)

# vulkan-gemm-ab: the VK-C tactic A/B — cooperative matrix vs the portable scalar
# GEMM, SAME BINARY with VT_VULKAN_COOPMAT as the only variable. Built only in a
# Vulkan build, because it links the Vulkan context directly to report which
Expand Down
36 changes: 36 additions & 0 deletions include/vt/device.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,42 @@ constexpr const char* DeviceTypeName(DeviceType device) {
return "unknown";
}

// The inverse of `DeviceTypeName`: resolves a lowercase spelling back to its
// enum, returning false when nothing matches. It lives here, beside the forward
// direction, for the same reason that one does — it names every platform
// EQUALLY, so it is a data list rather than a device-specific branch, and adding
// a platform still touches one enum and this one file.
//
// It exists because the shared `vllm` layer has to honour a device NAME an
// operator typed (`VLLM_CPP_VOCODER_DEVICE`, #672) WITHOUT either spelling a
// device enumerator or casting an integer into one. Both are exactly what
// `scripts/check-device-leakage.py` counts, and it is right to: a cast hardcodes
// a device by ENUM VALUE, never writes the token, and silently re-points if the
// enum is ever reordered. Enumerating the list HERE keeps that hazard inside the
// seam that owns the enum, and the static_assert makes adding a platform without
// listing it a build error rather than a silent gap.
inline bool DeviceTypeFromName(const char* name, DeviceType* out) {
if (name == nullptr || out == nullptr) return false;
constexpr DeviceType kAll[] = {DeviceType::kCPU, DeviceType::kCUDA, DeviceType::kMETAL,
DeviceType::kVULKAN, DeviceType::kXPU, DeviceType::kROCM,
DeviceType::kTENSTORRENT};
static_assert(sizeof(kAll) / sizeof(kAll[0]) == kNumDeviceTypes,
"DeviceTypeFromName must list every DeviceType");
for (const DeviceType device : kAll) {
const char* a = name;
const char* b = DeviceTypeName(device);
while (*a != '\0' && *a == *b) {
++a;
++b;
}
if (*a == '\0' && *b == '\0') {
*out = device;
return true;
}
}
return false;
}

struct Device {
DeviceType type = DeviceType::kCPU;
int32_t index = 0;
Expand Down
Loading
Loading