feat: Gemma 4 12B multimodal duo — default main+multimodal serve (#69) (v0.32.1) - #72
Conversation
/think frame for making `lobes serve` default to the Qwen3.6-27B-MTP + Gemma 4 12B NVFP4 duo. Gemma takes a new 'multimodal' role (vision + audio); tiers reframed to main/minor/multimodal; native MTP on by default with the DeepSeek DSpark draft as a disabled experiment; the Qwen3-14B is demoted to a legacy candidate. Five accepted plan risks parked for /spec-to-plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
/spec-to-plan forward leg: 9 tasks across 5 dependency waves covering all 26 spec targets, 6 accepted-risk objects. Pressure-ladder seam (main/minor/multimodal) resolved as task t6 (degrade to minor under pressure). Ready for /assign-to-workforce. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
Add a _RULES entry matching "gemma-4" / "gemma4" → "pythonic" so that
`infer_parser("sakamakismile/gemma-4-12B-coder-fable5-composer2.5-MTP-NVFP4")`
returns "pythonic". Gemma 4 (Google DeepMind) uses a Python-style
function-call syntax; vLLM's Gemma 4 recipe prescribes
--tool-call-parser pythonic.
Markers are scoped to "gemma-4" / "gemma4" to avoid matching older
Gemma 1/2/3 checkpoints whose tool-call story is less established.
Unit test added to tests/test_parser.py asserting the exact model id.
TODO(risk r2): confirm parser value against the served checkpoint
during t7 live validation on the Spark.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
… demotion - Add SupportedModel for sakamakismile/gemma-4-12B-coder-fable5-composer2.5-MTP-NVFP4 (role_hint=multimodal, tool_parser=pythonic, modelopt_fp4, native MTP speculative_config, status=configured). TODO(risk r4): confirm the Gemma4 native-MTP method string in t7. TODO(risk): confirm native context (using 128K). - Demote nvidia/Qwen3-14B-NVFP4 from role_hint=middle to candidate (KEPT, not deleted). - Reframe TIER_ROLE to main/minor/multimodal with cheap/normal/hard back-compat aliases; normal+multimodal -> Gemma, main+hard -> primary, cheap+minor -> 4B. - Add docs/gemma-4-12b-nvfp4.md stub (full content in t9). - Extend tests/test_catalog.py with Gemma-gear, tier-resolution and 14B-demotion tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
…B->candidate) Integration fix after t2 keystone: the 14B is now a legacy candidate and normal->multimodal. Catalog-domain, owned by no single task. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
…profile, DSpark off
- New vllm-multimodal service (NO profiles: key, always starts with the fleet)
serving sakamakismile/gemma-4-12B-coder-fable5-composer2.5-MTP-NVFP4 at
util=0.12, 128K context, tool-call-parser=pythonic, --speculative-config
gemma4_mtp, --trust-remote-code; NO --language-model-only so vision+audio stay
active (the headline capability).
- Gateway wired: MULTIMODAL_BASE_URL / MULTIMODAL_SERVED_NAME added to gateway
environment; vllm-multimodal added to depends_on.
- vllm-middle comment updated to "LEGACY CANDIDATE – no longer the normal tier".
- env.example: MULTIMODAL_* section added; DSpark draft-model override documented
as a commented, disabled-by-default experiment; GPU budget comment updated to
the new default (primary 0.45 + multimodal 0.12 + embed 0.06 + rerank 0.06 = 0.69).
- Three new template assertions in test_cli_fleet.py: multimodal is default-on
(no profiles:), command lacks --language-model-only and has gemma4_mtp
speculative-config, and vllm-middle is behind a profiles: key.
TODOs left: TODO(risk r4) — confirm Gemma4 native-MTP method in t7;
TODO(risk r5) — measure real multimodal util (vision+audio KV) in t7.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
…egacy profile, DSpark off
…imodal wiring - _config.py: replace _DEFAULT_MIDDLE/MIDDLE_BASE_URL/MIDDLE_SERVED_NAME with _DEFAULT_MULTIMODAL (pinned Gemma 4 12B id) / MULTIMODAL_BASE_URL / MULTIMODAL_SERVED_NAME; backend name changes from "middle" to "multimodal" (default url http://vllm-multimodal:8000). The 14B Qwen3 is now legacy — no tier resolves to it; address it explicitly by model id if needed. - _routing.py: fix tier_aliases() to handle the new mixed-vocabulary TIER_ROLE (primary: main/minor/multimodal + back-compat: cheap/normal/hard). The old algorithm relied on ascending-capability key order which the new TIER_ROLE breaks. New algorithm: sort unique roles by last occurrence in tier_role.values() (the back-compat aliases appear last in ascending order, anchoring the correct [minor, multimodal, primary] sequence), then apply upward fallback on unique roles only, and replicate results to all tier aliases. All six tier names (main/minor/multimodal + cheap/normal/hard) now resolve correctly with proper upward fallback when a gear is absent. - tests/test_gateway_tiers.py: full rewrite — 6 old "middle" tests renamed/updated to "multimodal"; 7 new tests added (main->primary, multimodal->Gemma backend, normal->multimodal back-compat, multimodal absent falls back to primary, primary vocab assertions in full-fleet tests, new tier_aliases helper test for minor- absent/multimodal-present escalation). 23 tests total, all passing. - tests/test_gateway_server.py, tests/test_tier_request.py: update _fleet_cfg / _full_fleet helpers to use MULTIMODAL_BASE_URL/MULTIMODAL_SERVED_NAME. - tests/test_status_pressure.py: fix pre-existing break from TIER_ROLE reframing (_MIDDLE_ID → _MULTIMODAL_ID for the normal tier; was failing before this PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
…->multimodal wiring + tier_aliases fix
…le restores legacy
… (main/minor/multimodal vocab) Resolve the pressure-ladder seam under the #69 main/minor/multimodal vocabulary. multimodal is a different capability (vision+audio), not a cheaper rung below main, so "downgrade under pressure" cannot walk main->multimodal->minor. The only cheaper target is minor: under degraded pressure (swap>75% OR iowait>50%) both a main and a multimodal request collapse to minor (reason=pressure); below the degraded floor nothing is downgraded (the old linear intermediate band is gone). Back-compat cheap/normal/hard inputs still normalize to the new vocab. - _pressure_policy.decide: binary degraded model in new vocab; max_allowed_tier is minor when degraded else main; allowed_tier granted as requested when warm, minor when degraded. Adds normalize_tier(); keeps the degraded thresholds + env overrides and retains the no-hard/prefer-cheap thresholds as advisory (no longer cap the tier — no intermediate rung). Docstring documents the seam. - _tier_request: served_tier reported in main/minor/multimodal; override path normalizes; is_tier_alias recognizes all six tier keys (catalog.TIER_ROLE). - status.py: lobes status --pressure queries with requested_tier=main and reports tier/model in the new vocabulary. - Migrate pressure/tier-request tests (test_pressure_policy, test_tier_request, test_status_pressure, test_gateway_server) off the old cheap/normal/hard->cheap degraded behavior. Full suite green (774 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
… minor (new vocab)
…ab (#69) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
…LET... Expand the stub file docs/gemma-4-12b-nvfp4.md into a COMPLETE per-model doc for the Gemma 4 12B 'multimodal' fleet gear (issue #69), closely matching the structure and tone of the sibling doc docs/qwen3-14b-nvfp4.md. Read these for accuracy BEFORE writing: lobes/catalog.py (the SupportedModel entry whose id is sakamakismile/gemma-4-12B-coder-fable5-composer2.5-MTP-NVFP4 — role_hint=multimodal, tool_parser=pythonic, quantization=modelopt_fp4, speculative_config gemma4_mtp, status=configured), lobes/templates/fleet/docker-compose.yml (the default-on vllm-multimodal service and its flags), and docs/qwen3-14b-nvfp4.md (template). The doc MUST cover: (1) what it is — Gemma 4 12B unified multimodal (text+IMAGE+AUDIO via Gemma4UnifiedForConditionalGeneration), native MTP draft head, NVFP4; (2) the pinned checkpoint id; (3) tier alias usage: model=multimodal (and model=normal back-compat) resolves here at the gateway, role_hint=multimodal; (4) serving in the fleet: it is DEFAULT-ON (not a profile), served WITHOUT --language-model-only so vision AND audio are active, --tool-call-parser=pythonic, native-MTP --speculative-config, --gpu-memory-utilization 0.12; (5) the DSpark experiment — DISABLED by default, an env override swaps in deepseek-ai/dspark_gemma4_12b_block7; (6) a GPU budget table for the default fleet: primary 0.45 + multimodal 0.12 + embed 0.06 + rerank 0.06 = 0.69 on the 128GB GB10; (7) accepted plan risks pending t7 live validation on the Spark: exact checkpoint pick, the gemma4_mtp method string, the 131072 native context, and the measured GPU util; (8) a Related docs section linking gateway-fleet.md, qwen3.6-27b-text-nvfp4-mtp.md, and qwen3-14b-nvfp4.md. Write ONLY docs/gemma-4-12b-nvfp4.md. Do not touch any other file. Implement the task above in this repository. Rules: - Make the SMALLEST change that correctly satisfies the task. - Follow the repository's existing patterns, style, and conventions — read the neighbouring files first so your change reads like the surrounding code. - Keep edits lint-clean: respect the project's maximum line length and end every text file with exactly one trailing newline. - You may read, create, modify files, and run commands as needed. - Don't widen the scope: do exactly what was asked, nothing more. When you are done, call finish with a short summary of exactly what you changed and why.
…dal per-model doc)
…l swap) + topology wording Verification correction on the colleague-authored doc: DSpark is a speculative-decoding draft head, enabled via MULTIMODAL_SPECULATIVE_CONFIG, not by replacing MULTIMODAL_MODEL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
…eam) to the Gemma duo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
Colleague full-branch review (#69) flagged the duplicated tier->role map in _pressure_policy.py as a silent-drift risk. Add a sync assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
… nv26.04/26.05 (track in #71) Live validation on the DGX Spark: the Gemma 4 12B multimodal gear does not load on any released NGC vLLM image (gemma4_unified arch absent from vLLM 0.19.0/0.21.0 + Transformers 4.57.6/5.6.0). Gear stays status=configured; custom-image unblock tracked in #71. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
PR Summary by Qodofeat: Gemma 4 12B multimodal duo — default main+multimodal serve (v0.32.1)
AI Description
Diagram
High-Level Assessment
Files changed (39)
|
Code Review by Qodo
Context used✅ Tickets:
🎫 Custom vLLM image for the Gemma 4 12B multimodal gear (gemma4_unified) — start with nightly Transformers 🎫 Make Spark default serve Qwen3.6 + Gemma4 duo✅ Compliance rules (platform):
16 rules✅ Skills:
doc-test-alignment, cicd 1.
|
SonarCloud python:S1135 flagged three bare TODO(risk …) comments on PR #72, all marking deferred Gemma 4 12B live-validation work (native context, the native-MTP method string, the pythonic tool parser). That work is now blocked on a gemma4_unified-capable vLLM image and formally tracked in issue #71, so the comments are reworded from "TODO(risk …)" to "Risk … (pending #71)" — they cite the tracking issue instead of an untracked TODO. The premise of S1135 (untracked work rots) no longer holds, and the fix improves traceability. - lobes/catalog.py: 2 markers (native context default; native-MTP method) - lobes/runtime/_parser.py: 1 marker (pythonic parser confirmation) Comment-only; 158 catalog/parser/tier tests pass, black/isort/flake8 clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
…verrides Two regressions surfaced by Qodo review on PR #72 (#69 Gemma duo): 1. Middle backend unreachable. The #69 14B demotion removed the `middle` optional-backend wiring from build_config(), but the fleet compose still ships the `vllm-middle` service (profile `middle`), still exposes MIDDLE_BASE_URL / MIDDLE_SERVED_NAME on the gateway, and still documents "set MIDDLE_BASE_URL … so it routes 14B requests." Result: enabling the profile silently fell back to the primary. Re-wire the optional `middle` generate backend — reachable by explicit served name, and (because "middle" is not a TIER_ROLE role) deliberately NOT a tier alias. 2. Legacy-keyed operator override ignored. resolve_tier_request() normalizes a legacy tier (hard→main) before the alias lookup, so a GATEWAY_ALIASES override keyed only by a legacy name (hard/cheap/normal) was bypassed on the pressure-aware path (the default path, since PressureCache is always wired). build_config() now mirrors a tier-keyed override onto its vocabulary synonyms via _expand_tier_alias_synonyms(); explicit keys still win and non-tier custom aliases pass through untouched. Tests: +4 middle-wiring tests + 3 override-expansion tests in test_gateway_tiers.py, +1 end-to-end legacy-override test in test_tier_request.py. Full suite 802 passed, 6 skipped; black/isort/flake8/bandit clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR
|
Qodo review triageThanks for the review — went through all six findings: 2 fixed, 4 pushed back with reasoning. Fixed (both in
|



Implements #69 — makes
lobes serve/initdefault to a main + multimodal duo: the Qwen3.6-27B-MTP primary (main) plus a Gemma 4 12Bmultimodalgear (vision + audio), with the tier vocabulary reframed to main / minor / multimodal and the Qwen3-14B demoted to a legacy candidate.Built through the devague /think → /spec-to-plan → /assign-to-workforce chain (the converged spec + plan are committed under
docs/specs/anddocs/plans/). The 9-task plan was fanned out to parallel agents in isolated git worktrees with TDD-gated merges.What's in it
role_hint="multimodal"Gemma gear;TIER_ROLE→main/minor/multimodal(+cheap/normal/hardback-compat); 14B →candidate.infer_parserreturnspythonicfor Gemma 4 ids.vllm-multimodal(no--language-model-only→ vision+audio; native-MTP spec-config; util 0.12); 14B behind a legacy profile; DSpark a disabled-by-default env toggle.model=main/minor/multimodal(+ back-compat); fixed atier_aliases()ordering bug exposed by the mixed vocabulary.lobes init/servedefault to the duo;--single/--legacyrestores the legacy single-model scaffold.main/multimodal→minor(multimodal is a different capability, not a cheaper rung);status --pressurereports the new vocab.docs/gemma-4-12b-nvfp4.md+ fleet-topology / 14B-demotion / pressure-seam updates acrossgateway-fleet.md,qwen3-14b-nvfp4.md,validate-tiers.md,README.md,CLAUDE.md.Live validation (t7) — why the gear ships
configured, notload-testedt7 ran on the DGX Spark with zero fleet disruption (co-resident, primary/mesh untouched). The checkpoint downloads fine, but Gemma 4 12B's
gemma4_unifiedarchitecture does not load on any released NGC vLLM image — absent fromnv26.04(vLLM 0.19.0 / TF 4.57.6) andnv26.05.post1(vLLM 0.21.0 / TF 5.6.0). So the gear is honestlystatus="configured".The code is complete and correct; the live image/audio/MTP validation +
load-testedpromotion happen once a supporting image exists. The custom-image unblock (start with nightly Transformers) is tracked in #71. The smoke test's live assertions stay gated behindLOBES_SMOKE_BASE_URL.Quality
uv run pytest→ 794 passed, 6 skipped (skips: 3 live smoke gated on a running gateway, 3 numpy-absent audio parity).black/isort/flake8/bandit(0 medium, 0 high) all clean._TIER_ROLEdrift hazard between_pressure_policy.pyandcatalog.py, fixed with a sync test.Implements #69 (live load-test pending #71).
— lobes (Claude)
🤖 Generated with Claude Code
https://claude.ai/code/session_01K9gvEGUZaAUHey7mVHEHLR