Skip to content

drama: Drama 3 (fishaudio/drama-3) text-to-speech on CUDA - #920

Open
plotarmordev wants to merge 15 commits into
ashhart:mainfrom
plotarmordev:feat/drama3-cuda
Open

plotarmordev wants to merge 15 commits into
ashhart:mainfrom
plotarmordev:feat/drama3-cuda

Conversation

@plotarmordev

Copy link
Copy Markdown
Contributor

What this changes

This adds the Drama 3 text-to-speech family on CUDA (part of #914): backbone, tower and codec, a speech hook in
engine_api, the native fish_omni host, and POST /v1/audio/speech, which answers whole as WAV or raw PCM. The
codec reimplements Fish Speech's published modded-DAC design with no Fish Audio code copied, and the weights and
codec.pth stay under Fish Audio's licence and are not included.

This pull request is stacked on #915, #916 and #917; its first three commits carry the same patches. Its
own commits start at "drama: the Drama 3 text backbone on CUDA".

This overlaps #844 in CUDA build wiring, native family registration, engine capabilities, server integration, CUDA
tests and third-party notices. The recorded trial merge conflicts in zig/build/cuda.zig, zig/src/native/cuda.zig
and zig/tests/cuda/main.zig; the proposed order is #844 first, then rebase this stack and reconcile the capability
flags.

Receipt

  • Environment, with the TensorFold commit, MLX or PyTorch version, machine and GPU, checkpoint and revision:

    • Code. Head 10796d46f625, on main c7137bb3. No MLX or PyTorch.
    • Builds and host tests. An NVIDIA GB10 build host on Ubuntu 24.04.4 LTS aarch64 (Linux 6.17), with Zig
      0.17.0, nvcc 13.0.88 and CUDA_VISIBLE_DEVICES empty.
    • GPU measurements. A GB10 (sm_121) host with CUDA 13 libraries (cuBLASLt 13.1.1). Checkpoint
      fishaudio/drama-3 at e0a9218a, against the reference sglang-omni ab095ef9.
    • Which build was measured. This series built on TensorFold 1e76a70e, native binary sha256 aa211423….
      Here it is rebased onto c7137bb3, with comments made single-line and files split. Every Drama fatbin of this
      head is byte-identical to the measured series' at the same commit. The host Zig is token-identical apart from
      the file split, compared with std.zig.Ast.
    • Memory. On the final build of the series, the host's CUDA allocations at /health were 40,806,970,484 B
      active and 40,857,363,572 B peak (qualification.json, R7).
  • Exactness, from tools/bench_concurrent.py --alone --serial or token_sha against "draft": false: n/a, because
    there are no drafts and no text tokens. Fidelity against the reference is not qualified. These are selected
    component gates on the final build (qualification.json):

    Gate Bar Native Result
    Codec SNR, 1 frame ≥ 40 dB 33.086 dB fail
    Codec SNR, 2/7/128/1024 frames ≥ 40 dB — pass
    Backbone hidden relative L2 ≤ 0.05 0.124 fail
    Backbone worst-row cosine ≥ 0.8713 0.8006 fail
    Semantic KL ≤ 0.01 0.0243 fail
    Semantic top-30 overlap ≥ 0.9 0.867 fail
    Semantic top-1 equal — pass
    Tower 7-row streaming head top-1 equal 600 vs 463 fail (reference tie)

    The all-row cosine gate (0.99233 < 0.999) and last-row cosine gate (0.99670 < 0.999) also fail
    (qualification.json).

    Chunk invariance holds: backbone outputs at chunk sizes 1, 7 and 128 are hash-identical, and drama-qk and
    drama-conv pass.

  • Decode speed before and after, from tools/bench_openai.py: n/a, because that tool measures text tokens. Speech
    used the recipe's harness (before-after-02, speed-stages.json):

    • Real-time factor of the reference: 0.939.
    • This port plus streaming, before any speed change: 1.087, so slower than the reference. The speed pull requests
      bring the full series to 0.634.

    These are distinct benchmark populations: the pre-speed RTF uses the normal-item speed-stage corpus, while the
    reference/final comparison uses the warm before-after corpus. The reference and native audio durations differ; the
    reference failed the supplementary CUDA-free/MemFree admission check but was admitted by the pilot guard, and the
    effect of differing cache/admission conditions was not isolated (qualification.json).

  • Prompt speed before and after: the cold first request's ttft_s from tools/bench_cache.py --require-cold on a long prompt, or "n/a" with the reason when the change cannot reach prompt processing: n/a, because there is no
    prompt cache or token TTFT on the speech route. Process start to ready:

  • Tests run, with pass, fail and skip counts:

    • GPU audio gate on the stack's tip bef432bbb25e (one GB10, under the recipe's guard): all 46 normal-request
      audio bodies byte-identical to the promoted pre-upstream build, the long-05 clipped body identical in both
      passes, bench/stream_check.py 9/9, warm RTF 0.654 against the pre-speed 1.087, and all six tower one-row
      projections on drama_tower_gemv.
    • CUDA_VISIBLE_DEVICES= zig build test-cuda-host --summary all from an empty local cache: 29/29 steps, 484 passed, 0 failed, 0 skipped.
    • zig build test-golden --summary all: 4/4 steps.
    • zig build native -Dnvcc=/usr/local/cuda/bin/nvcc --summary all: 46/46 steps.
    • zig build fatbins -Dnvcc=/usr/local/cuda/bin/nvcc: the four Drama fatbins are byte-identical to the measured
      series' at this commit.
    • zig build STEP -Dnvcc=/usr/local/cuda/bin/nvcc --summary all for each drama-* tool step of this head:
      18/18 steps build.
    • python3 tools/zig/lean_check.py: 527 problems at every commit, main's 527 (lean_new.py: 0 new).
    • zig fmt --check: clean at every commit.
    • On the GPU (final build of the series, qualification.json): the codec, backbone and tower oracles above,
      tf-cuda-test drama-qk and drama-conv (pass), and a 24-item × 3 warm corpus run per engine with 0 failures.
  • Not run, and why:

    • Metal: the family is CUDA-only.
    • tools/bench_openai.py, bench_concurrent.py and bench_cache.py: they drive text tokens.
    • A measured concurrent-versus-solo receipt: requests queue one at a time, and it is proposed in Drama 3 (fishaudio/drama-3) text-to-speech on CUDA: family proposal and gate #914.
    • A GPU run on this exact head: the GPU audio gate ran once, on the stack's tip bef432bbb25e; this head is
      host-tested only. Its kernels are byte-identical to the measured build's at this commit.

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off. (Text
    families are unchanged. Drama 3 generates audio frames in its own host loop, one synthesis at a time, not through
    the lane rounds; Drama 3 (fishaudio/drama-3) text-to-speech on CUDA: family proposal and gate #914 raises this.)
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (No
    drafts or resume. Concurrent-equals-solo is not measured.)
  • No precision traded for speed. If the bits change, the description says which sums change and why. (The
    checkpoint's BF16 and block-FP8 are retained without requantization; the codec uses FP16. This is not a claim of
    identical arithmetic to every reference call shape: the backbone's canonical arithmetic and the reference's
    shape-dependent projections differ, and fidelity remains not qualified.)
  • Prompt processing is no slower than the last release. (Not measured. Text families' prompt paths are not
    touched.)
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency. (The new host
    tests pass. The oracle drivers need a GB10, the checkpoint and reference captures, and are not host tests.)
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

setAllocationLimit bounds the bytes of device buffers plus pinned host buffers. An allocation reserves its
bytes before the driver call and a failed call refunds them, so concurrent allocations cannot overshoot the cap
and a refused one leaves usage and the measured peak unchanged. Host counts now take the same lock as device
counts. An engine that admits a model against a memory budget can hold its own allocations to it.
The tower turns a backbone row into a frame's ten codebook values: four shared and nine residual layers whose
cache keeps every preceding text and audio row, a semantic head and the reference sampler (temperature, top-p over
the whole vocabulary, then top-k). Its projections run on cuBLASLt; drama_tower.cu holds the mixer, norms, RoPE,
attention and sampling kernels.
The codec decodes ten codebooks a frame to mono 44.1 kHz PCM, 2048 samples a frame, as the reference's FP16
vocoder does: RVQ, the windowed post-module transformer, two upsample stages and the DAC decoder. codec_pth.zig
reads torch.save's zip archive and runs its state-dict pickle on a VM that knows only the opcodes and globals a
tensor state dict uses, so nothing in the file runs. Weight norm is folded in FP32 and tensors are cast to FP16
with round-to-nearest-even, as the reference does.

codec.zig holds the decode; codec_load.zig holds the codec's geometry, the weight-norm folding, the FP16 casts
and the one device weight image.
An engine may now synthesize speech: SpeechRequest carries a prompt, sampling settings and caller-owned PCM,
Info.speech describes the audio it produces, and Info.text_generation lets a speech-only engine refuse the text
routes. Text submit gains error.Unsupported, which the chat route reports as a request error.
The native CUDA backend opens fish_omni checkpoints with the Drama 3 host: one synthesis at a time on a worker
thread that owns the context, stream, backbone, tower and codec, admitted against the same memory pool the text
families use. The pool reading moves to cuda_memory.readPool so both paths share it, and the host caps its own
allocations at what it was admitted for. Options the host cannot honor are refused at open.
OpenAI's speech request, built into Drama 3's default-voice prompt as the reference's prompt builder builds it
and synthesized whole, then sent as WAV or raw PCM. Fields the route would silently drop are refused, and a
speech-only engine starts the server without the text-only services.
Drivers that check the native backbone and tower against captured reference tensors, replay single operators
against them, and trace each stage for attribution, plus tf-cuda-test drama-conv and drama-qk on synthetic rows.
None of them runs as part of the host tests.
Drivers that decode captured codes with the native codec for comparison with the reference decoder, replay its
RVQ and weight preparation in isolation, and trace each decoder stage. None of them runs as part of the host tests.
…ve host and their tool steps

Embeds the backbone and tower fatbins, adds the drama module to the native server and its host tests, and a build
step per backbone and tower oracle, replay and trace driver. The codec's kernels and tools follow on their own.
Embeds the codec fatbin and adds a build step per codec oracle, replay and trace driver. With this the series
builds.
Some file systems accept an O_DIRECT open and then fail the aligned read with EINVAL. The reader now clears
O_DIRECT on that descriptor and reads again through the page cache instead of failing the load; a test injects
the refusal.
A marker test: an image can carry a device global that promises something about its kernels, and the host can
check for it before relying on the promise. Any driver failure, a missing name included, answers false rather
than an error. A test drives it through a fake driver table.
Drama 3's backbone is a Qwen3.5-35B-A3B-style hybrid: 30 Gated DeltaNet and 10 gated full-attention layers,
each with a 256-expert top-8 block-FP8 MoE and a gated shared expert. Model.run appends rows and returns every
row's final-norm hidden state and the last row's semantic logits.

The kernels follow the arithmetic of the sglang-omni reference: the canonical 64-token chunked GDN prefill
(drama_gdn_chunk.cuh, after flash-linear-attention's kernels as SGLang carries them), the packed recurrent decode,
gated attention with Q/K norm and partial NeoX RoPE, dynamic 1 x 128 FP8 activation quantization, and the BF16
tensor-core GEMMs in drama_backbone_gemm.cu. Weights load as stored; nothing is requantized.

THIRD_PARTY_NOTICES.md credits flash-linear-attention (MIT), whose chunked kernels the GDN prefill is adapted from
and whose packed decode the recurrence follows.
@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thank you, @plotarmordev, we will carry the Drama backbone, tower and native codec work with your credit into the combined Drama family; the landing order and remaining checks are in our reply on #927.

Posted by Ash's AI Assistant Synthena

@ashhart ashhart mentioned this pull request Oct 17, 2026
Conflicts, each kept from both sides:
- zig/build/cuda.zig: family() returns radio and drama; engines() and
  nativeServer() take core, heat and drama; main's tf-cuda-test runner,
  kernel contract and GLM/LFM2/RADIO helpers stay ahead of the drama tool steps.
- zig/src/core/engine_api.zig: the speech types beside main's vision types;
  SubmitError holds Unsupported and Stale; the VTable keeps submit_speech
  beside recall, imprint and drain.
- zig/src/native/cuda.zig: families lists lfm2_vl then fish_omni; pool() and
  weightBytes() leave for cuda_memory's readPool() and weightBytes(); openDrama
  refuses main's new --vision, --precision, --record and --learn-conversations.
- zig/src/server/{chat,root,routes}.zig, zig/tests/cuda/main.zig: both sides'
  error arms, imports and test files.
- zig/src/server/speech.zig: imports the named json module (ashhart#860).
…n types in their own files, Drama's refusals in cuda_refusal

Upstream main's engine_api.zig is at 600 lines and native/cuda.zig at 583, so the merge took them to
632 and 602. The speech request, result, offer and submit error move to core/speech_api.zig, and
the image and video input types to core/vision_api.zig, re-exported under their names as
rows_ask.zig's are. Drama 3's flag refusals join LFM2's in cuda_refusal.zig, with a test. No
declaration, behavior or test is removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants