Skip to content

Commit 905498f

Browse files
committed
Add multimodal support to the portable driver (Metal/CPU)
Bring the portable ggml driver to multimodal parity with the CUDA driver, validated on Metal and CPU: - Qwen3-VL vision: ViT encoder (full attention, blocked 2D RoPE, learned pos-embed interpolation, 2x2 patch merger, deepstack mergers), single and multiple images per forward, and video. - Qwen3-VL language-model M-RoPE: true per-token [t,h,w] positions via ggml_rope_multi, merging the 1-D positions with the wire's image_mrope_positions (mirrors the CUDA merge). Text tokens reduce to plain RoPE, so text-only generation is unchanged. - Gemma-4 vision (SigLIP-style ViT) and audio input (Conformer encoder), single and multiple clips. - CSM-1B TTS: backbone + fused single-graph depth RVQ decoder + Mimi vocoder. CSM loads in f32 (the sequential depth decode is precision-critical); codes are bit-exact vs the HF reference. - No silent CPU fallback: the driver announces its backend and flags a CPU landing; PIE_PORTABLE_STRICT_METAL hard-errors on CPU-offloaded ops. A load warning fires when a multimodal model runs on an unvalidated GPU backend. - SDK: Context::append_images / append_audios for N-per-forward packing. - Weight-loader fixes: strip leading singleton dims on shape compare; group tensors by pointer to dodge the ggml 64-char name cap. Docs in driver/portable/MULTIMODAL.md. New e2e test inferlets multi-image-test and multi-audio-test.
1 parent 8824d3e commit 905498f

36 files changed

Lines changed: 4046 additions & 24 deletions

driver/portable/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ add_library(pie_driver_portable_lib STATIC
9393
src/plan.cpp
9494
src/graph_common.cpp
9595
src/graph_qwen3.cpp
96+
src/graph_vision_qwen3vl.cpp
97+
src/graph_vision_gemma4.cpp
98+
src/graph_audio_gemma4.cpp
99+
src/graph_csm_gen.cpp
100+
src/graph_csm_mimi.cpp
96101
src/graph_gemma4.cpp
97102
src/graph_qwen3_5.cpp
98103
src/graph_phi3small.cpp

driver/portable/MULTIMODAL.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Portable driver: multimodal support
2+
3+
The portable (ggml) driver runs the same multimodal model scope as the CUDA
4+
driver. Validated on **Metal and CPU**; see "Backend scope" below for other
5+
backends.
6+
7+
| Capability | Model | Notes |
8+
|-------------------|------------------|--------------------------------------------------|
9+
| Vision (image) | Qwen3-VL, Gemma-4| single + multiple images per forward |
10+
| Vision (video) | Qwen3-VL | frames sampled host-side; M-RoPE temporal axis |
11+
| Audio input | Gemma-4 | Conformer encoder; single + multiple clips |
12+
| Audio output (TTS)| CSM-1B | backbone + depth RVQ decoder + Mimi vocoder |
13+
14+
## Architecture
15+
16+
Preprocessing is host-side (`runtime/src/multimodal.rs`): decode, resize,
17+
patchify, mel, and the `(t,h,w)` patch grid / M-RoPE positions, matching the HF
18+
processors. The driver receives f32 pixel values / log-mel features plus anchor
19+
rows, runs the vision/audio encoder, and scatters the resulting soft-token
20+
embeddings into the language model's token-embedding rows at their anchors. The
21+
wire format already carries everything (`image_*`, `audio_*` slices in
22+
`pie_bridge` / `view.hpp`); no ABI changes were needed.
23+
24+
Per-modality graph builders:
25+
26+
- `graph_vision_qwen3vl.cpp` — ViT (full bidirectional attention, blocked 2D
27+
RoPE, learned pos-embed interpolation, 2x2 patch merger, deepstack mergers).
28+
- `graph_vision_gemma4.cpp` — SigLIP-style ViT (2D RoPE, avg-pool, sandwich
29+
norms, attention scale 1.0).
30+
- `graph_audio_gemma4.cpp` — Gemma-4 Conformer audio encoder.
31+
- `graph_csm_gen.cpp` / `graph_csm_mimi.cpp` — CSM backbone + fused depth
32+
decoder + Mimi vocoder.
33+
34+
## Key design decisions
35+
36+
**Qwen3-VL M-RoPE.** The language model applies true per-token `[t,h,w]` M-RoPE
37+
via `ggml_rope_multi` (`graph_qwen3.cpp`, gated on `hparams.use_mrope`). The
38+
executor merges the 1-D positions (text rows → `[p,p,p]`) with the wire's
39+
`image_mrope_positions` (image/video rows → `[t,h,w]`) in
40+
`build_mrope_positions_`, mirroring the CUDA driver. `graph_common.cpp` lays the
41+
4×-wide `pos_input` out global-axis-major because the dense graph ropes the whole
42+
batch at once. Text-only tokens have `t=h=w=p`, so M-RoPE reduces to plain RoPE
43+
and text generation is unchanged. Qwen3.5's separate per-request mrope path is
44+
untouched.
45+
46+
**CSM keeps f32.** CSM's 31-step sequential RVQ depth decode is precision
47+
critical: bf16 weights flip near-tie argmaxes and the error compounds, drifting
48+
the audio codes off the reference. The model is small, so CSM is loaded in f32
49+
unconditionally (`model.cpp`, `keep_f32`). With f32 the codes are bit-exact vs
50+
the HF reference (96/96). This is a deliberate correctness-over-speed tradeoff
51+
(~2x backbone decode vs bf16).
52+
53+
**Fused CSM depth decoder.** The 31 codebook steps run in one graph with in-graph
54+
`ggml_argmax` chaining each step's code into the next step's embedding, threading
55+
per-layer KV handles across the unrolled steps. Each step reads exactly its valid
56+
cache prefix (`Lkv = p+1`, a compile-time constant) so the attention reduction
57+
width matches the per-step reference bit-for-bit. This collapses ~31 host-argmax
58+
syncs/frame to one compute.
59+
60+
**No silent CPU fallback.** The driver announces its selected backend at load and
61+
flags `(!! no GPU backend active)` if it landed on CPU when a GPU was expected.
62+
`PIE_PORTABLE_STRICT_METAL` turns an unsupported-op scatter into a hard error
63+
instead of a quiet partial CPU offload.
64+
65+
## Backend scope
66+
67+
The multimodal graph builders are compiled into the portable driver on every
68+
backend, but are **validated on Metal and CPU only**. CSM and Mimi use
69+
`ggml_backend_graph_compute` directly, so an op the active backend doesn't
70+
support aborts mid-encode rather than falling back. On non-Metal GPU backends
71+
(notably Vulkan) the driver emits a startup warning that multimodal is
72+
unvalidated there. CPU works because the CPU backend implements every op.
73+
74+
## Diagnostics
75+
76+
- `PIE_CSM_DUMP=1` — dump per-frame CSM codebook indices.
77+
- `PIE_PORTABLE_KEEP_F32=1` — keep all f32 source weights in f32 (no bf16
78+
downcast); useful for isolating precision from logic.
79+
- `PIE_PORTABLE_STRICT_METAL=1` — hard-error on any CPU-offloaded op.
80+
81+
## Tests
82+
83+
End-to-end inferlets exercise each modality: `image-qa`, `video-qa`,
84+
`multi-image-test`, `audio-qa`, `multi-audio-test`, `tts`. Vision and audio-input
85+
are validated by output correctness; CSM TTS is validated bit-exact against a
86+
saved reference frame set.

driver/portable/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ drivers.
77
Use this driver for CPU inference, local development, or non-NVIDIA backends
88
supported by ggml.
99

10+
Multimodal support (Qwen3-VL / Gemma-4 vision, Gemma-4 audio input, CSM-1B TTS)
11+
is documented in [MULTIMODAL.md](MULTIMODAL.md).
12+
1013
## Build
1114

1215
The normal path is through the `pie` binary:

driver/portable/src/arch_spec.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ ArchSpec arch_spec_for(PieArch a, const Hparams& h) {
9898
ArchSpec s;
9999
switch (a) {
100100
case PieArch::Qwen3:
101+
case PieArch::Qwen3VL:
102+
// Qwen3-VL's text decoder is plain Qwen3 (q/k norm). The vision
103+
// tower is handled separately (load_vision_tower_qwen3vl_).
101104
s.has_qk_norm = true;
102105
break;
103106
case PieArch::Qwen2:

0 commit comments

Comments
 (0)