Files
esh-pfi-infrastructure/stacks/voices-seat/compose.yaml
T
Vuong Hoang 6692701571 docs(voices-seat): every voice prefixes an empty think block unless the caller disables it
Found while smoke-testing the lv-mccarthy ship. Measured live:

  default                 -> '<think>\n\n</think>\n\nThere were no horses in the road...'
  enable_thinking=false   -> 'The sun was hot on the dry riverbed and the stones were red...'

This is the Qwen3-4B-Instruct CHAT TEMPLATE, not an adapter property, so it applies
to lv-yarros, lv-bronte and lv-hemingway equally and has done since this seat went
up on 2026-09-16.

No gate number is affected: gen_beats_chat_yarros.py sets enable_thinking when the
template supports it, so every arm in every r49 gate was generated without the tags.
But a caller that does not pass chat_template_kwargs gets 17 junk characters at the
head of every passage -- and any word-count or in-band check run over that string is
counting the tags as prose. Skaldsong should be checked.
2026-09-21 18:02:56 -07:00

226 lines
13 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# voices-seat — one base carrier, N author LoRA adapters, hot-swappable. fv-ml1 GPU 0, :8027.
#
# NAMING: adapters are `lv-<author>` — lv for lang-voice (operator, 2026-09-16, retiring the
# `baby*` prefix: it read fine for one experiment and invites confusion across a family).
# The adapter NAME is the request's `model` field, so this string is the public API of a voice.
#
# WHY LORA AND NOT MERGE (operator decision 2026-09-16, after measurement): the R49 line
# produces a FAMILY of author voices — lv-bronte, lv-yarros (shipped), lv-hemingway (in
# build) — and Skaldsong switches between them per request. Merging bakes each 264 MB adapter
# into a fresh 7.6 GB model:
#
# 3 authors LoRA 7.6 GB + 3x264 MB ~ 8.4 GB merged ~23 GB
# 6 authors LoRA 7.6 GB + 6x264 MB ~ 9.2 GB merged ~46 GB
#
# On a box where GPU 1 has 5.7 GB free, GPU 2 has 2.4 GB and GPU 3 is a held reserve, that
# is the whole argument. A new author becomes a directory drop, not a VRAM negotiation.
#
# ⚠ SUPPORT WAS CHECKED, NOT ASSUMED. The training-throughput playbook records a LoRA refusal
# on a Qwen3 *MoE* arch ("get_expert_mapping must be implemented") and its lesson is that
# feature support is per-architecture, not per-family. Verified on the fleet's own engine
# before committing: vllm/model_executor/models/qwen3.py:271 declares Qwen3ForCausalLM with
# SupportsLoRA, plus packed_modules_mapping and embedding_modules. Dense Qwen3 is fine; do NOT
# transplant this compose onto an MoE carrier without re-running that grep.
#
# ⚠ KV IS PINNED IN BYTES, deliberately, following gen-small-seat's note. On THIS box
# `--gpu-memory-utilization` has been measured wrong in both directions — cyberprev at 0.40
# holds 47.1 GB (8 GB over its fraction), gen-small at 0.48 holds 36.9 (10 GB under) — so a
# fraction cannot be used to place a seat beside live co-tenants. GPU 0 already carries
# cyberprev, gen-small and the Parakeet STT seat; an explicit KV budget makes this seat's
# footprint deterministic instead of negotiated.
#
# ⚠ ADAPTERS ARE MOUNTED READ-ONLY and named explicitly. A request selects a voice by putting
# the adapter name in the `model` field — switching voices is a field, not a deployment.
#
# MEASURED on this seat, 2026-09-16, rather than taken from docs:
# POST /v1/load_lora_adapter {"lora_name","lora_path"} -> 200 in 0.24 s
# POST /v1/unload_lora_adapter {"lora_name"} -> 200 in 0.003 s
# VRAM unchanged across the swap; container stayed healthy; no restart, no reload.
# ⚠ Anything added that way is GONE on `compose up -d` unless it is ALSO listed in
# --lora-modules below. Runtime load is for trying a voice; this list is what survives.
#
# MEASURED LORA COST on this seat (n=30 per arm, interleaved, A-vs-A floor 0.1%):
# base median 143.0 tok/s
# lv-yarros median 108.2 tok/s -> -24.3%, far outside the floor.
# Accepted deliberately: the seat is for prose, not a latency path, and 24% buys every future
# author for 264 MB instead of 7.6 GB. If a voice ever lands on a hot path, merge THAT one.
#
# MEASURED RESIDENCY: 10,740 MiB, against a requested 0.11 x 94.97 GiB = 10,700 MiB — a 40 MiB
# miss on a box where the util fraction has been wrong by 8-10 GB in BOTH directions. Pinning
# --kv-cache-memory in bytes is what makes the fraction predictive; do not remove it.
# ⚠⚠ CALLERS MUST SEND `chat_template_kwargs: {"enable_thinking": false}` OR EVERY VOICE
# PREFIXES AN EMPTY `<think></think>` BLOCK TO ITS OUTPUT. Measured live 2026-09-21 on
# lv-mccarthy, and it is a property of the Qwen3-4B-Instruct CHAT TEMPLATE rather than of any
# adapter -- so it applies to lv-yarros, lv-bronte and lv-hemingway too, and has done since
# this seat went up on 2026-09-16:
#
# default -> '<think>\n\n</think>\n\nThere were no horses in the road...'
# enable_thinking=false -> 'The sun was hot on the dry riverbed and the stones were red...'
#
# The r49 gate harness passes it (gen_beats_chat_yarros.py sets enable_thinking when the
# template supports it), which is why no gate number is affected -- but a caller that does NOT
# pass it gets 17 junk characters at the head of every passage, and a word-count or an
# in-band check run over that string is measuring the tags as prose. Check Skaldsong.
name: voices-seat
services:
vllm-voices:
image: ${VOICES_IMAGE:-vllm/vllm-openai:nightly-e9d1398d9edfd90fcc1cf783805240e3effec013}
container_name: ${VOICES_CONTAINER_NAME:-vllm-voices}
restart: unless-stopped
ipc: host
ports:
- "${VOICES_PORT:-8027}:8000"
volumes:
- /tank/aimodels/huggingface:/hfcache
- ${VOICES_MODEL:-/tank/aimodels/Qwen3-4B-Instruct}:/model:ro
- /tank/aimodels/voice-adapters:/adapters:ro
environment:
- VLLM_API_KEY=${API_KEY:-}
- VLLM_ALLOW_RUNTIME_LORA_UPDATING=${VOICES_RUNTIME_LORA:-1}
command:
- /model
- --served-model-name
- ${VOICES_SERVED_NAME:-voices-base}
- --host
- 0.0.0.0
- --port
- "8000"
- --gpu-memory-utilization
- "${VOICES_GPU_MEM_UTIL:-0.12}"
- --kv-cache-memory
- "${VOICES_KV_CACHE_MEMORY:-2147483648}"
- --max-model-len
- "${VOICES_MAX_MODEL_LEN:-8192}"
- --max-num-seqs
- "${VOICES_MAX_NUM_SEQS:-8}"
- --dtype
- auto
- --enable-prefix-caching
- --enable-lora
# ⚠ This is the max LoRAs in a single BATCH, not the number registered — 4 adapters are
# listed below and that sits exactly at the default. A FIFTH voice does not need a bump to
# be registered, but it will queue rather than batch alongside the other four.
- --max-loras
- "${VOICES_MAX_LORAS:-4}"
- --max-lora-rank
- "${VOICES_MAX_LORA_RANK:-32}"
- --lora-modules
- lv-yarros=/adapters/lv-yarros-4b-v1
# lv-bronte: +0.193 delta_cb toward held-out Brontë, 8-gram overlap 0.00
# (identical to the never-saw-it control). Shipped 2026-09-17 as additive,
# reversible and safety-clean.
#
# ⚠ IT WAS SHIPPED AS A VOICE-AXIS FAILURE AND THAT VERDICT NO LONGER STANDS.
# As run, the floor was the largest within-arm seed spread across ALL THREE
# arms = 0.251, contributed entirely by ckpt925 — a candidate that was NOT
# shipped — on one outlier seed. The floor rule was changed to PAIRWISE on
# 2026-09-17 (pre-registered for lv-hemingway before any of its numbers
# existed, on the grounds that a candidate's verdict must not depend on which
# other arms happened to be generated). Re-scored under that rule, on the
# SAME generations, with the same delta_cb values:
#
# ckpt475 (this adapter) +0.193 vs a pairwise floor of 0.091 -> PASSES, 2.1x
# ckpt925 (not shipped) +0.210 vs its own spread 0.251 -> still fails
#
# The previous session found this defect, wrote it down, and deliberately did
# NOT exploit it — choosing the floor that passes your preferred answer after
# seeing the numbers is the exact failure pre-registration exists to prevent.
# The rule was fixed prospectively instead; this follows from it.
# See /adapters/lv-bronte-4b-v1/README.md and
# persistent-memory.d/2026-09-17-lv-bronte-gate.md
- lv-bronte=/adapters/lv-bronte-4b-v1
# lv-hemingway: checkpoint-850 (epoch 0.959), NOT the loss minimum at step 1750.
# The two are indistinguishable on voice — 0.072 apart against a 0.113 pairwise
# floor — so the tiebreak fell to the axes that resolve, and 850 wins all of them:
# 2.3x tighter seed spread (0.050 vs 0.113), lower memorisation (0.07 vs 0.08),
# less ran-on (0.08 vs 0.12), and half an epoch less overfit.
#
# VOICE ✅ +0.413 delta_cb at 6.4x the pairwise floor — the strongest result in this
# line, closing 73.8% of the span between the unadapted carrier and held-out
# Hemingway itself. It also clears the OLD all-arms floor (0.113), so this verdict
# does not depend on the 2026-09-17 rule change.
# DAMAGE ✅ ran-on +0.08 and on-beat −0.14, both inside a 0.217 floor.
#
# ⚠ MEMORISATION IS THE AXIS TO READ BEFORE QUOTING THIS ONE AS CLEAN. 0.07 hit-rate
# against a base control of 0.00 — but that control is weak here, because base writes
# summary and cannot collide with a register it does not imitate. The honest
# reference is the author himself: HELD-OUT HEMINGWAY scored against the train split
# collides at 0.01. So the adapter reproduces train n-grams ~7x more often than
# Hemingway reproduces himself. Every one of the 19 matched runs was READ: all are
# stock dialogue ("came over and sat down at the table"), max 9 words, no proper
# noun, no plot, no imagery — and 9 is shorter than the 10-word run unseen Hemingway
# shares with the train split by coincidence. Elevated rate, zero protectable
# content. Hemingway is in copyright; lv-yarros is the in-line precedent, also in
# copyright, shipped at 0.10 against a 0.07 control. Unload is 0.003 s.
# See /adapters/lv-hemingway-4b-v1/README.md,
# scripts/hemingway-corpus/GATE-PREREG.md (pre-registered before any generation),
# persistent-memory.d/2026-09-17-lv-hemingway-gate.md
- lv-hemingway=/adapters/lv-hemingway-4b-v1
# lv-mccarthy: checkpoint-300 (epoch 0.652), NOT the loss minimum at step 900.
# ⭐ The two-epoch recipe is now 0 for 3, and this time the loss curve was CONFIDENT and
# wrong: ckpt900 is the minimum and ckpt300 sits +15.8x the median neighbour jitter above
# it, yet ckpt300 wins every axis that resolves — 3.2x vs 1.2x voice margin, 2.7x tighter
# seed spread, memorisation 1.0x the author's rate vs 1.8x, ran-on 0.13 vs 0.28.
#
# VOICE ✅ +0.177 delta_cb at 3.2x the pairwise floor — the BEST arm of five, on both the
# primary read and the pre-registered punctuation-stripped secondary (+0.128 at 2.8x), so
# ~3/4 of the gain is words rather than marks. Closed 60.8% of the reachable span.
# ⚠ This corpus's voice axis is DELIBERATELY harder than Brontë's or Hemingway's: the
# `mccarthy` register names its own punctuation tics and the eval drives the base control
# with the same prompt, so the adapter earns no delta for "emit no quotation marks".
#
# NOT COPIED ✅ THE CLEANEST SAFETY RESULT IN THIS LINE. 0.12 hit-rate against HELD-OUT
# McCARTHY'S OWN 0.12 — indistinguishable from the author — and its longest verbatim run
# (10 words) is SHORTER than the one genuinely unseen McCarthy shares with the train split
# by coincidence (12). All 31 matched runs were READ: stock grammar in the commonest words
# ("looked at the wolf and he looked at the boy"), and the name-shaped hits are the RENAMED
# invented names. Nothing protectable. McCarthy is in copyright with a LIVING ESTATE.
# ⚠ The base-unadapted control is defective and its 0.00 is unearned — base writes summary
# and cannot collide with a register it does not imitate. Against base this reads as a 12x
# red flag; against the author it is 1.0x. Use the author, not base.
#
# ⚠⚠ THE COST, AND THE GATE'S RULE IS BLIND TO IT. Axis C is ran-on only (ratified
# 2026-09-15 because the carrier maxes the other terms) and this adapter passes it with 40%
# headroom — but IN-BAND IS 0.65 AGAINST BASE'S 0.89 and on-beat 0.42 against 0.71.
# **35% of its generations miss the requested 90-140 word band, against base's 11%.**
# Base is GOOD at length here, so the adapter measurably makes direction-following worse.
# ⚠ No checkpoint choice fixes this — every adapted arm is 33-48% out-of-band and ran-on is
# non-monotonic in epoch (0.38 -> 0.13 -> 0.20 -> 0.28); ckpt300 is the measured minimum.
# The fix, if the cost bites, is a retrain targeting length, not a different checkpoint.
# IF A CALLER DEPENDS ON A WORD BUDGET, EXPECT ONE GENERATION IN THREE TO MISS IT.
# Unload is 0.003 s and one compose line.
#
# See /adapters/lv-mccarthy-4b-v1/README.md,
# scripts/mccarthy-corpus/GATE-PREREG.md (pre-registered before any generation, plus three
# dated amendments — axis C as first frozen was ARITHMETICALLY UNSATISFIABLE),
# scripts/mccarthy-corpus/gate-results/ (raw output, all five arms),
# persistent-memory.d/2026-09-21-lv-mccarthy-gate.md
- lv-mccarthy=/adapters/lv-mccarthy-4b-v1
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids:
- "${VOICES_GPU_ID:-0}"
capabilities: [gpu]
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 20
start_period: 300s
labels:
- homepage.group=AI - Inference
- homepage.name=Voices
- homepage.icon=mdi-account-voice
- homepage.description=Author voice adapters (LoRA) on Qwen3-4B
- homepage.href=http://10.251.50.54:8027/docs
networks: [tnet]
networks:
tnet:
name: traefik-net
external: true