ComfyUI 0.24.1 added native attention selection; the node-based
BlehGlobalSageAttention errors "does not support the new ComfyUI attention
changes". Add --use-sage-attention to COMFY_CMDLINE_EXTRA so the in-image
sageattention v2.2.0 sm_86 build (rebuilt vs pinned torch 2.12.1) binds via
the native path. OOM flags preserved. Deployed to irv-ml1 + recreated; log
confirms "Using sage attention", container healthy, serving 200.
(comfy-dev request, thread 01KVE89T2DKC)
Operator consolidation (2026-06-18): give ComfyUI the full 48 GB A6000 and move the
audio/TTS zoo to the 3090.
- comfyui: NVIDIA_VISIBLE_DEVICES all -> 1 (A6000 only), + DISABLE_UPGRADES=true to
pin torch at 2.12.1+cu129 so the mmartial boot script stops auto-upgrading it and
the compiled SageAttention kernels stay matched (comfy-dev torch-pin, approved).
- parakeet: NVIDIA_VISIBLE_DEVICES all -> 0 (3090).
Other GPU reassignments are deployment-side (not repo compose): chatterbox-fast via
its .env CBF_GPU_DEVICES=0; vibevoice device_ids ["1"]->["0"] (deployed from
/worktank/vibevoice/build); yt-voice-clipper worker via its override. dia2-2b,
ace-step, csm-expressiva downed (stale/unused).
Result: A6000 = ComfyUI alone (48.3 GB free); 3090 = chatterbox + parakeet + the
on-demand audio (vibevoice/ytvc/kokoro). SageAttention rebuilt against the pinned
torch; OOM cmdline (COMFY_CMDLINE_EXTRA) preserved; /object_info still lists the 9
acceleration nodes.
Serves the in-house abliterated Mistral Small 4 (heretic NVFP4, vision-intact)
under --served-model-name mistral-small-4 on ana-ml2 GPU0:8010 — a true drop-in
for the official mistral-small-4 backend. Both litellm entries (mistral-small-4 +
mistral-small-4-reasoning) route here with no litellm change.
GPU0 fits one mistral-class model, so this is a backend swap, not a co-tenant:
bring up after downing the official stack; revert by downing this and up-ing the
official. Verified live through the gateway: standard returns clean answers,
reasoning populates reasoning_content (the [THINK] split). Checkpoint built per
tools/mistral-small4-nvfp4/.
GLM 5.2 released ~2026-06; confirmed reachable with our existing
Z_AI_API_KEY (z.ai /models lists glm-5.2; a live completion returned
clean). Added two model_list entries mirroring the glm-5.1 pattern:
glm-5.2 (thinking DISABLED by default, per the 2026-06-11 operator call)
and glm-5.2-reasoning (thinking ENABLED, opt-in). Deployed to
ana-docker /opt/docker/conf/litellm/config.yaml, litellm restarted,
both verified through the gateway (disabled -> reasoning_tokens 0;
reasoning -> 234).
/healthz catalog_version read the BAKED package version (importlib.metadata),
so a catalog/frontend-only webhook deploy (no image rebuild) left it stale —
v0.12.4 data went live but /healthz still reported 0.12.3. comfy-dev's v0.12.5
reads comfy_catalog.__version__ from the repo-root pyproject.toml; mounting it
on the same checkout mount (catalog/graphs/frontend) makes /healthz report the
MOUNTED version after a catalog-restart. Falls back cleanly if absent.
Pushed to irv-ml1's host compose + validated via `docker compose config`
(bind -> /app/pyproject.toml:ro resolves). Recreate deferred to comfy-dev's
imminent v0.12.5 rebuild (the mount is inert for v0.12.5 itself, which bakes
its own version — it only matters for subsequent catalog-only deploys — so no
separate prod blip). Requested by comfy-dev (engine owner), althing thread
01KV95R88A3Y.
vLLM's OpenAI server 400s on an empty tools array ("tools must not be an
empty array"), which broke every gateway call carrying tools:[] (clients
that send it to mean "no tools" -- OpenAI tolerates it, vLLM does not).
drop_params doesn't help: it drops unsupported PARAMS, not empty VALUES.
Add a CustomLogger async_pre_call_hook (conf/strip_empty_tools.py) that
pops an empty/None tools field (+ orphaned tool_choice) before forwarding,
registered globally via litellm_settings.callbacks so it covers every
vLLM-backed model, not just mistral-small-4. Mounted at
/app/strip_empty_tools.py beside config.yaml (LiteLLM resolves callbacks
relative to the config dir). Surgical: only fires when tools is present
and empty; real tools pass through untouched.
Verified on live gateway (1.87.0): mistral-small-4 and granite-4.1-8b
with tools:[] now 200 (were 400); no-tools baseline unchanged; a real
tool still passes through.
Put GPU1's idle ~11 GB buffer to work on the two KV-bound models that gained
live consumers from the worldtree migration (granite + the pooling models
under-use their util, so growing them is wasted):
- selene 0.13→0.17: KV 2.53→6.33 GiB, concurrency 1.27x→3.16x @32K (Domari judge)
- qwen36 0.32→0.34: KV 7.73→9.63 GiB, concurrency 2.92x→3.64x @131K (arbo judge +
worldtree actor/echo + gateway)
GPU1 free now ~5.6 GB (safe floor for single-service recreates).
selene: AtlaAI Selene-1-Mini-Llama-3.1-8B judge restored on vLLM after the
llama-swap teardown took its Q6_K GGUF offline. FP8 (dynamic --quantization
fp8; FP8 >= the validated Q6_K fidelity, and text-only Llama so no vision-
tower-noise risk; NVFP4's W4A4 too aggressive for a precision judge). GPU1
util 0.13 (8.51 GiB weights + 2.53 GiB KV, 32K ctx, 1.27x concurrency),
~11 GB GPU1 buffer left. Gateway selene-1-mini-8b → :8011 (shadows the *
wildcard that used to reach it via llama-swap). Judge smoke: scored an
unfaithful claim 1/5 correctly.
mistral-small-4: max-model-len 131072 → 262144 (full native 256K) for
novel-length consistency-checking. KV pool is util-bound (~862K tokens), so
256K costs no extra VRAM — max concurrency just drops to 3.29x at full length.
max-num-seqs 64 → 32 keeps the warmup transient flat (scales with seqs × len),
so it fits the tight GPU0 (free unchanged at 5.2 GB). Verified loaded + healthy.
Operator needs a verified-working vision tower as the abliteration/tuning
baseline. vLLM 0.23.0 crashes Mistral multimodal at startup (#44911
fetch_images regression, ~0.22.1+). Pinned the Mistral container to
v0.22.0 — the last pre-regression release — which loads the NVFP4
(compressed-tensors) AND serves vision: verified a half-blue/half-red
image read correctly ('left blue, right red'). Dropped --limit-mm
(vision re-enabled). qwen36 stays on 0.23.0 (separate container; needs it
for its ModelOpt NVFP4).
- gateway: add mistral-small-4-reasoning. Operator asked for effort=medium
but Mistral's reasoning_effort is BINARY (none/high only — medium 400s);
set to 'high' (sole reasoning-ON level). NOTE: reasoning fires but
reasoning_content-splitting is unreliable on v0.22.0 (lands in content);
clean split would need 0.23.0, which breaks vision — vision prioritized.
- mistral-small-4 (instant) + mistral-small-4-reasoning both gateway-live.
Mistral-Small-4-119B-2603-NVFP4 (119B/6.5B-active MoE, 65.3 GiB) on the
freed GPU 0 (dedicated 96 GB Blackwell), vLLM 0.23.0, :8010. NVFP4 is the
only variant that fits one card (FP8 ~119 GB / bf16 ~238 GB need 2 GPUs).
- TEXT-ONLY: vLLM 0.23.0's Mistral multimodal processor crashes at startup
(fetch_images bug); loaded with --limit-mm-per-prompt image/video=0.
Remove the flag to restore vision once vLLM patches it.
- MLA attn (TRITON_MLA), mistral tool-call + reasoning parsers, util 0.93,
max-len 131072 (capped from native 256K), image pinned by 0.23.0 digest.
- litellm: mistral-small-4 → :8010, shadows the * wildcard.
- GPU 0 reassigned from the (now-offline) llama-swap zoo per operator.
The qwen3.6-35b-a3b VL checkpoint is a single hybrid model with a per-
request enable_thinking switch (Qwen3-style), defaulting thinking ON.
Make the default non-thinking and add an opt-in reasoning variant,
mirroring the existing glm-5.1 / glm-5.1-reasoning gateway split.
- qwen36-vl compose: add --reasoning-parser qwen3 (model-matched) so the
single :8007 endpoint splits <think> into reasoning_content when on and
routes all output to content when off — serving both modes cleanly.
- litellm gateway: base qwen3.6-35b-a3b pins chat_template_kwargs
enable_thinking=false (non-thinking default); new qwen3.6-35b-a3b-thinking
pins enable_thinking=true (opt-in reasoning). Same upstream checkpoint,
no extra VRAM/container.
Deployed + verified on ana-ml2 (vLLM recreated, healthy) and ana-docker
(litellm reloaded): default returns a direct answer with no reasoning_content;
-thinking returns cleanly-separated reasoning_content, no raw tag leak.
Retire qwen35-vl (Qwen3.5-9B); add qwen36-vl serving the official FP8
Qwen3.6-35B-A3B vision MoE on :8007 under its TRUE name only — no alias.
qwen3.5-9b-fp8 is killed at vLLM AND the litellm gateway (404/400); a model is
never served under a prior model's name. Consumer (comfy-dev/arbo) notified +
migrated; arbo vkeys flipped to all-proxy-models; shared all-agents-local key
repointed to qwen3.6-35b-a3b.
GPU-1 rebalance for the heavier FP8 weights (~34 GB): granite 0.35->0.24 /
131K->64K, embed/rerank 0.05->0.03 (reclaimed util-reservation waste). Verified:
vision correct, 20-concurrent/endpoint load test = no OOM (~7.5 GB headroom).
Drop the llama-swap qwen3.5-9b GPU-0 pin (GPU 0 freed for the creative-writing
hot-swap card). NVFP4 was the lighter fit (~21 GB) but its vLLM ModelOpt-MoE
loader is broken (KeyError w2_input_scale / lm_head.input_scale, vllm #44081);
revisit when fixed.
Operator's call: keep the arbo stack in eshpfi and version its deploy machinery
alongside the compose (was host-only on irv-ml1 = recoverability foot-gun).
- arbo-webhook.py: :9009 HMAC listener (secret externalized to host file, not git)
- arbo-deploy.sh: internal-route fetch + catalog-only targeted restart
Document both in the README Q5 section + the internal-gitea-route gotcha.
Operator decision 2026-06-13 (relayed by comfy-dev, confirmed in-session):
turn off the prod arbo engine's bearer auth and rely on the WireGuard
perimeter. Reverses ADR-0001's open-auth-hole-closed posture (comfy-dev owns
the ADR update on the vh/arbo side).
The app's protected-gate no-ops only when ENGINE_TOKEN is ABSENT — an empty
string still gates (verified: ENGINE_TOKEN="" -> /workflows 401). So both
inject paths are removed: the compose environment line is commented out and
the .env line deleted on the host. Result: tokenless GET /workflows 200 (was
401), matching the dev engine. Original token preserved in the host's
.env.pre-auth-off.bak for re-enable.
playbooks/arbo-disable-engine-token.yaml captures the reversible procedure.
The arbo-prompt-enhance vkey was extended to reach qwen3.5-9b-fp8 for the
hero auto-judge step (v0.11.3+), not granite-only. Confirmed via /v1/models
for the key. Docs-only; no version bump.
ana-ml2 was upgraded 2026-06 from dual RTX 6000 Ada (46GB, cc 8.9) to
dual RTX PRO 6000 Blackwell Max-Q (96GB, cc 12.0 / sm_120). Update the
stale hardware facts across the workspace:
- CLAUDE.md servers table row
- servers/ana-ml2/README.md hardware spec (+ refreshed system-details.txt)
- stacks/vllm compose + .env.example FP8/KV comments (Ada cc 8.9 -> Blackwell cc 12.0)
- stacks/llama-swap config VRAM-budget comment (48GB -> 96GB, GPU-0 pin)
Also corrects the adjacent stale 'Phi-4-mini' comment in the granite
service block (the service has been Granite 4.1 8B since 34a43a0).
Doc/comment-only; no runtime change.
Benched granite prefix caching at ~6.5x faster TTFT (45ms cached vs 292ms
uncached) on a shared ~4.5k-token summarizer template. granite already had it
on by vLLM-v1 default; pinned explicit so a version flip can't silently disable
it. qwen (nightly) defaulted it OFF -> flipped on (free for the text-chat path,
marginal for vision where each image is a distinct prefix). Soft/evictable KV,
zero memory change (GPU1 still ~3.7GB free), all 5 services healthy.
ComfyUI's ~325G model tree moved off the near-full worktank NVMe (97%->26%,
342G free) to /storetank/arbo (roomy SATA SSD on irv-ml1), overlay-mounted
back at /basedir/models so ComfyUI behaviour is unchanged. rsync byte-verified
(src==dst), one comfyui restart, worktank original removed. Inventory of the
set in docs/arbo-comfyui-model-catalog.md for the retain decision. The older
919G /storetank/image-models/comfy archive is untouched (separate reclaim).
Qwen3.5-9B vision-language served FP8 on ana-ml2 GPU1 (co-located with
granite + the embed/rerank/reward trio; GPU0 kept free for hot-loading
large models), :8007, fronted by LiteLLM as qwen3.5-9b-fp8.
Pinned to vllm/vllm-openai nightly@sha256:49211ab2 — :latest (v0.19.1)
quantizes the VL vision tower under fp8 and garbles vision; the nightly
correctly excludes it (LM stays FP8, vision tower BF16). util 0.40
(~38GB) on the shared card (vLLM needs free>=util*total here). Vision
verified end-to-end through the gateway.
SGLang 0.5.13 confirmed to support our formats on Blackwell sm_120
(compressed-tensors NVFP4 W4A4, fp8, modelopt_fp4, petit_nvfp4, fp4_e2m1 KV),
so the bench can be a real NVFP4 head-to-head. Parameterized compose (model/
quant/GPU via .env) + a common streaming load generator (bench.py: agg tok/s,
TTFT p50/p99, TPOT) so both engines are driven identically on an exclusive GPU.
Bench-oriented; promote to a real stack only if SGLang wins. Launch deferred
until the NVFP4 eval frees a GPU.
Ada->Blackwell swap doubled card VRAM, so the Ada-era fractions (0.07/0.07/
0.18) reserved ~2x the bytes for the same models. Empirically re-floored via
0.01-step climb until each service was stable under 20x parallel inference:
embed/rerank 0.05 (load-floor for the 0.6B models), reward 0.10 (the real
over-provision). Frees ~11 GB on GPU 1. Live .env on ana-ml2 already applied.
ana-ml2's Ada->Blackwell swap (2x96GB) frees GPU 0 entirely. Pin llama-swap
to GPU 0 via NVIDIA_VISIBLE_DEVICES so on-demand large-model hot-loads land
there, off GPU 1 where the always-on vLLM services (granite + embed/rerank/
reward) live. Closes the long-standing 'pin llama-swap to GPU 0' item.
Extends the catalog/graphs git-pull-mount pattern to the SPA frontend so
frontend changes reach prod via git pull + restart, no image rebuild.
Delivers the v0.11.2 auth-on catalog-load fix without a rebuild; baked
image frontend stays the fallback.
New stack mirroring the canonical convention for the Arbo (catalog) engine,
co-located beside comfyui on irv-ml1 per ADR-0001 D1/D3:
- engine<->ComfyUI over traefik-net container DNS (http://comfyui:8188),
SSH dependency eliminated; file ops bind the shared basedir input/output
- named local-disk volumes for the gallery SQLite (arbo_db, restic-backed)
and hero images (arbo_heroes); catalog as a ro git-checkout mount (D2)
- ENGINE_TOKEN + GRANITE_KEY via on-host .env; GRANITE via the LiteLLM gateway
- Q5 catalog-pull: manual day-1, ytvc-style webhook follow-on
Image build, /healthz, catalog in-container path, and non-root UID are
comfy-dev's to confirm (CONFIRM items in README).
glm-5.1 now disables GLM thinking by default via extra_body (LiteLLM strips
top-level thinking under drop_params but forwards extra_body verbatim to z.ai).
New glm-5.1-reasoning alias = same upstream with thinking enabled, so reasoning
is opt-in. Operator call 2026-06-11; primary driver is the pi coding harness.
Verified live: glm-5.1 reasoning_tokens=0, glm-5.1-reasoning reasoning_tokens>0.
LLM observability for the fleet — pretty trace UI over the gateway: prompts,
completions, reasoning, latency, token counts. The pretty layer LiteLLM's
spend_logs lacked.
- stacks/langfuse: v3 self-host stack (web/worker/postgres/clickhouse/redis/
minio) on ana-docker, adapted from upstream. UI on :3001 (gitea owns :3000).
Project + API keys auto-provisioned via LANGFUSE_INIT_*. HOSTNAME=0.0.0.0 on
langfuse-web so it's reachable via the published port while also on tnet.
- litellm: enabled success_callback/failure_callback: ["langfuse"] (the
passthrough env was already wired); keys + host go in the litellm .env.
Verified: stack healthy, project keys authenticate, and a real gateway call
landed a litellm-acompletion trace in Langfuse within ~6s. Secrets live only in
the server .env (never committed).
Granite 4.1 8B beat phi4-mini on precision in brokkr's R15 P03 eval, so it's
the new production summarizer/dreamer for nevermore.
- vllm-phi4 -> vllm-granite: official IBM FP8 (ibm-granite/granite-4.1-8b-fp8,
compressed-tensors), GPU 1, 50K ctx, FP8-KV, CUDA graphs. Same :8004 slot.
- GPU 1 retune: the embed/rerank/reward trio was over-provisioned (embed ran a
5.89x KV pool, reward 3.90x). Trimmed utils 0.20/0.20/0.30 -> 0.07/0.07/0.18,
freeing ~10 GB so granite runs with CUDA graphs (not --enforce-eager) and
keeps ~10 GB free as a hedge for future Granite text-LoRAs (--enable-lora).
- LiteLLM: phi4-mini model_list entry -> granite-4.1-8b (hosted_vllm @ :8004);
explicit entry shadows the '*' wildcard's llama-swap route.
- nevermore repointed (LLAMA_SWAP_MODEL=granite-4.1-8b via the gateway) live.
Verified end-to-end: vLLM :8004 generates, gateway routes (gateway-granite-ok),
KV 86,768 tokens/1.69x at 50K, 0 restarts, GPU 1 10.3 GB free.
ComfyUI's default cudaMallocAsync allocator phantom-OOMs ('allocation
would exceed allowed memory', 0 bytes allocated) when the A6000 is
shared with the pinned TTS services (~22 GB used). --disable-cuda-malloc
switches to PyTorch's native allocator. --fp8_e4m3fn-text-enc loads the
FLUX.2 Qwen3-8B text encoder as fp8 (~8.7 GB) instead of upcasting the
fp8 file to fp16 (~16 GB), matching the box's Ampere-fp8 posture.
Applied via COMFY_CMDLINE_EXTRA in the canonical compose; verified the
allocator flipped to 'native' and both flags are on the live cmdline.
IBM Granite 4.1 dense instruct (3B + 8B), unsloth Q4_K_M GGUF via -hf
syntax, 64K ctx with q8 KV. Auto-exposed through the LiteLLM gateway
wildcard (ana-docker:4000) and direct on llama-swap (:9292).
Replaces the retired irv-ml1 Ollama granite4.1 — Ollama is now banned
fleet-wide; serving consolidates onto the sanctioned llama-swap/vLLM
substrate.
Adds explicit gateway entries for the four z.ai GLM models (glm-5.1,
glm-5-turbo, glm-4.7, glm-4.5-air) routed to api.z.ai with Z_AI_API_KEY,
plus the compose env passthrough + .env.example doc. Explicit entries
win over the llama-swap wildcard (distinct IDs, no collision). Extends
the gateway's unified logging to cloud inference, not just local
vLLM/llama-swap.
Cost note: paid API — only gateway-keyed callers reach these, but calls
spend z.ai credits (documented in config + compose comments).
Adds a `model_name: "*"` entry routing any unmatched model to llama-swap
(ana-ml2:9292) so its whole swappable LLM zoo logs through the gateway
without per-model registration — add/swap models in llama-swap freely,
litellm logs them all. Exact entries (phi4-mini/qwen3-embedding/
qwen3-reranker → vLLM) still win; the wildcard only catches the rest.
litellm does no inference; llama-swap keeps loading + serving. Enables
routing worldtree-personal's generative chat through the gateway for
full req/resp logging while preserving llama-swap's on-demand swapping.
LiteLLM proxy fronting the vLLM services on ana-ml2 so every request +
response is captured and inspectable in a browser Logs UI — the
visibility vLLM itself lacks (Dozzle shows only connection metadata).
- compose: litellm (proxy + /ui Logs) + litellm-db (Postgres store)
- conf/config.yaml: routes phi4-mini (chat, :8004), qwen3-embedding
(:8001), qwen3-reranker (:8002); store_prompts_in_spend_logs persists
full prompt/completion text. reward classifier (:8003) stays direct
(no first-class LiteLLM route).
- Langfuse-ready: lean first cut intentionally skips Langfuse's heavy v3
stack; graduating is one env-var + callback step, no re-architecture.
- roadmap: mark the vLLM-observability item's first cut as shipped.
Lean first cut of docs/roadmap.md "Observability for the vLLM stack".
Operator chose option (ii): keep the OFFICIAL Phi-4 format globally rather than
impose Ollama's leaner scaffold on every phi4 consumer. Removes the
--chat-template override + the conf/phi4-chat-template.jinja file (90e08f0).
vLLM now uses the tokenizer's built-in template (system <|end|> present);
verified 7-token render via tokenize/detokenize. brokkr re-baselines its R15
canonical on the official scaffold so baseline == production.
vLLM's official Phi-4 tokenizer template emits <|end|> after the system turn;
Ollama's does not. That single boundary token regressed brokkr's R15 P02
admission eval (type macro-F1 -33pp) vs the Ollama-measured canonical, while
valid_format held at 1.0. Operator chose to make vLLM match Ollama's leaner
scaffold globally (baseline == production). Adds conf/phi4-chat-template.jinja
(drops the system <|end|>) + mounts it + --chat-template on vllm-phi4. Applied
prompt verified via tokenize/detokenize; brokkr re-smokes probe_vllm.yaml.
chatterbox-fast is authored software with a test suite, not a config-mirror stack —
so it moves to its own MIT-licensed, versioned, CI'd repo (gitea vh/chatterbox-fast,
v0.1.0) following the sister-repo pattern. Replace stacks/chatterbox-fast/ with a
pointer README; the moved code (scheduler/app/bench/tests/Dockerfile/compose) now
lives in the new repo. The deployed :8197 service is unaffected (still runs the
legacy devnen-based image; self-contained-image migration is an optional follow-up).
The fleet catalog entry stays in docs/asset-engine/services.yaml.
- chatterbox-fast experimental -> ready: browser audition verified end-to-end
(operator confirmed progressive playback "excellent" 2026-06-02).
- vibevoice ready -> down: no container running on irv-ml1 (connection refused);
catalog status was stale.
- voxtral: NOT a stale typo — its stack genuinely claimed :8197, the port now held
by the live chatterbox-fast. voxtral is down, so moved IT to :8201 (catalog
endpoint + source_url, stacks/voxtral/.env.example + README, host .env) rather
than disturb the live service. No live clash existed (voxtral down) but it was a
latent deploy-time collision I introduced by placing chatterbox-fast on 8197.
No catalog_version bump (status changes + endpoint correction, additive). Validates
against the schema.
TTSRequest gains `seed` (0=random); seeded once per request under the lock via
torch.manual_seed + cuda.manual_seed_all. One-shot output is then byte-reproducible
for a fixed seed+params (verified: seed=42 -> identical sha256 across runs).
Streaming stays non-reproducible by design — adaptive-chunk boundaries depend on
live-measured RTF. Needed for the asset-engine catalog reproducibility contract
(parity with the chatterbox sibling, which exposes seed).
stream=false + format=wav emitted the streaming 0xFFFFFFFF-length header, so a
buffered consumer reading a complete wav got bogus RIFF/data sizes. One-shot knows
the full length, so emit correct sizes; streaming keeps the open-ended header
(length genuinely unknown up front). Verified remote: one-shot wav data size ==
bytes-44, python wave.open() reads 2.20s cleanly; streaming still 0xFFFFFFFF.
Deployed on irv-ml1 beside live chatterbox (:8196): healthy on :8197, TTFB ~0.5s,
no starvation. Measured VRAM 5.34 GB (fp32) settles the placement: the 3090's
~3.8 GB free does NOT fit, A6000 (device 1) is the only viable card.
Revert the priming feature from d707439. Live A/B caught an audible artifact: the
context-priming discard-cut left part of the throwaway prefix in the output, so a
clause ("...without a trace of sarcasm,") was spoken an extra time.
Root cause is structural: generate() returns one finished waveform with no marker
for where the prefix ends, and the model renders the same prefix with different
timing when followed by content than when generated solo — so the duration-estimate
+ energy-minimum cut is a guess and can leave a sliver (or a whole clause) of prefix
in. A reliable cut would need token-level access (the abandoned native-streaming
arc) or a per-chunk ASR/alignment pass (heavy, still imperfect, eats the latency
budget). Fails the agreed bar: "keep only if it closes the gap without a seam."
Kept from d707439: the .gitignore (build artifacts). NOT re-applied: the bundled
margin_first fix — wiring it would shrink chunk 1 (more joins = worse coherence),
against the operator's priority, and margin=0.8 there is already starvation-safe.
Coherence loss at joins stays an accepted limitation; cold streaming was judged
"really good". Phase 1 + Phase 2 parity/perf untouched. Next: Phase 3 deploy.
Prime early joins by prepending the prior sentence as backward prosodic context,
generating context+content together, then discarding the context audio. The cut
snaps to the inter-sentence pause (energy-minimum search around the context's
solo duration) with a 5ms fade-in to kill any seam click (app: _cut_at_pause /
_fade_in / Engine.generate_primed). Opt-in via request `prime` (default off).
Scheduler: priming is AFFORDABILITY-GATED so it can never starve. A primed chunk
costs ~(2·context + content)/rtf (a 2nd context-solo pass); a chunk is only primed
when buffer ≥ prime_buffer_factor (1.5) × that cost, else it falls back to a cold
generate. Consequences proven in the GPU-free sim (17 tests):
- fires on early joins for any GPU at/above rtf_prior (3.4 = 3090; A6000 ~3.8-4.0)
- self-skips (degrades to cold) on a slower-than-fleet GPU rather than starving
- never primes chunk 0 (latency-critical)
Also fixed a latent Phase-1 bug: margin_first was applied at chunk 0 (budget always
0 there) so it never did anything — now applied at chunk 1 (the first transition).
Live A/B on irv-ml1 (A6000, GLaDOS): TTFB unaffected (445 vs 467ms), no starvation;
priming fired on chunk 2 (gen 1.6s for the doubled pass). On typical text exactly
ONE early join safely primes — priming chunk 2 flattens the buffer so later/larger
chunks no longer clear the safety gate. Samples: ~/chatterbox-ab/_p2_{cold,primed}.wav.
- /voices endpoint lists predefined voice stems (excludes `_`-prefixed bench/A-B
scratch wavs); shared _predefined_wavs() also feeds default-voice discovery.
- Perf levers: TF32 matmul/cudnn + flash/mem-efficient SDPA, default ON, env-gated
(CBF_TF32 / CBF_SDPA_FLASH). Startup logs model dtype.
Measured on irv-ml1 (turbo, A6000): the model loads FLOAT32 (not the fp16 older
notes assumed). TF32+SDPA do NOT move TTFA (489->514ms, noise) — first-sentence
latency is bound by the sequential AR token decode at batch-1, not matmul
throughput. bf16 (the lever that would help) is DEFERRED: from_pretrained() has no
dtype arg and turbo's fp32 conditioning path + dtype-sensitive vocoder make a
clean cast nontrivial; not worth the quality risk at ~0.5s TTFA. torch.compile
also deferred (batch-1 regression). Findings recorded in README.
Voice management parity (predefined dir + per-request clone refs) was already in
the Phase-1 resolve path; /voices completes the surface.
Build the streaming TTS server MVP per docs/design/chatterbox-fast-plan.md §4.
- scheduler.py: adaptive buffer-ratchet chunker (the meat) — GPU-free pure
logic. First sentence emitted alone for low TTFA, then chunks ratchet ~3x by
packing whole sentences to margin x buffered-audio; drives off measured RTF +
sec/char (EMA). relieve_leader() clause-splits a too-big mid-stream sentence
to avoid starvation (joins land on commas); a long comma-less sentence is the
one honored-but-flagged limitation.
- test_scheduler.py: GPU-free simulation, 13 tests — asserts no-starvation
(incl. overestimated RTF) and the ratchet.
- app.py: FastAPI model holder + POST /tts StreamingResponse (raw PCM s16le
default, wav optional, stream/oneshot) + GET /health.
- bench.py: client — ground-truth TTFB + real 1x-consumer starvation check.
Live test on irv-ml1 (turbo, A6000, GLaDOS voice): streaming TTFB 499ms vs
oneshot 5230ms (~10x), stayed ahead of a 1x player (no starvation), ratchet
1.64->4.08->8.60->8.60s audio, measured RTF self-corrected 3.38->4.01.
Kill the superseded docs/design/chatterbox-fast.md — its §5 windowed-token
streaming was the abandoned native-frame-streaming arc; the adaptive-chunk plan
supersedes it. Repoint persistent-memory + README at the canonical plan.
reference_id=<name> resolves against the DIRECTORY references/<name>/
(audio + same-basename .lab), not a flat references/<name>.wav. Voices
were staged flat with the per-name dirs left empty, so every
reference_id resolved to nothing and Fish fell back to its default
speaker — every dropdown voice produced byte-identical audio (proven:
Abigail == Imogen == no-ref, same text+seed). This was the real "no
accent" root cause, independent of the asset-engine "undefined" select
bug.
Server fix (applied to irv-ml1): populated references/<name>/<name>.wav
+ <name>.lab for all 32 voices; re-test confirms Imogen/Eleanor/
Beatrice/Abigail/no-ref now all distinct.
Durable hardening + record correction:
- playbook: normalize-layout step (flat <name>.wav -> nested dir, cp -u
idempotent, when-gated on count mismatch) + an A/B verify gate that
hard-fails the deploy if two reference_ids yield identical output.
- services.yaml: correct the reference_id resolution doc (dir + .lab,
not flat wav).
- README + persistent-memory: correct the "reference_id-by-name is THE
working path, verified" claim — it was a no-op until this fix; the
prior ECAPA 0.79 result came through the inline base64 path.
Tear down the parked CSM stack (status: down, never successfully built).
Bring-up attempts failed at the image build: upstream
phildougherty/sesame_csm_openai pins no huggingface_hub version, which now
resolves to 1.17.0 where the `huggingface-cli` the Dockerfile relies on has
been removed (replaced by `hf`). Building would require vendoring + patching
the upstream Dockerfile.
Deep-research verdict (primary + community sourced) confirmed it isn't worth
that: the acclaimed Maya/Miles demo runs a fine-tuned, larger CSM variant
Sesame never open-sourced; the open csm-1b is the un-fine-tuned 1B base
(only the smallest of 1B/3B/8B shipped, no newer checkpoint as of mid-2026).
Ships no usable voices, can't generate text, English-only, can't stream
real-time out of the box; absent from current TTS leaderboards and dominated
by Kokoro/Dia2/Fish-S2/IndexTTS for narration.
Removes: stacks/csm/, playbooks/deploy-csm.yaml, the csm catalog service +
reproducibility_audit entries. Host state (compose dir, /worktank/csm) torn
down on irv-ml1; no container/image existed.