Repository navigation
drama3: Fish Audio Drama 3 text to speech on one CUDA GPU (new family, plain decoding; proposal) - #939
drama3: Fish Audio Drama 3 text to speech on one CUDA GPU (new family, plain decoding; proposal)#939drowzeys wants to merge 7 commits into
Conversation
|
@drowzeys For the "Other GPUs: … sm_89 … never run" line: #939's kernels on a real sm_89 card. RTX 4090 (24 GB), Windows 11, driver 616.56, nvcc 13.3, How it was built. #939 targets Linux, and this machine has none, so I merged #836 (the Windows port) into
The kernel contract on sm_89 (
On the live launches:
Not from #939. The run's summary is 10 of 13 kernels passing gates 1–4, with 1 limit problem. All of those are kernels already on
Not verifiable on a 24 GB card: frames/s, first audio, the oracle check and memory. The checkpoint is 38.7 GiB, so the model doesn't fit. |
cwschroeder
left a comment
There was a problem hiding this comment.
Host-side review of 2cfd812b14f9b9f28b76ffa0d8018f7806cce33d, based on main 46a08c18. The five family tests are wired into test-cuda-host; I ran them and the module wrapper. Three reachable issues are below.
Small warmup fix, separate formatter patch and CPU reproducers. The warmup fix is e17e03ac5e7e20f22538e288d42fad0229483571; it bounds the actual decode to its output allocation. The harness calls the checkout's exact Tts.create with inert transports: four small-chunk cases fail before the fix, and all six cases pass in both Debug and Fast afterward. It does not perform CUDA or codec arithmetic.
The unchanged PR head also fails the pinned zig fmt --check build.zig zig on config.zig, kernels.zig and codec.zig. Separate commit b34800ef47ec24debafb61f5a1a5de756f256484 applies that formatter. On both fix commits together, all four exact No GPU test commands, the family tests, full-tree format, whitespace and lean comparison pass locally with Zig 0.17.0. Lean is 517 versus 517, zero new. The intentionally failing output-error diagnostic is separate from those passing CI tests.
Output-error propagation and codec-only admission remain unfixed in the offered patches. Upstream workflow runs currently conclude action_required; the No GPU run 38901868963 has zero jobs, so there is no executed remote test failure to diagnose. No GPU, model, audio quality or performance qualification is claimed here.
| var zero = try cuda.DeviceBuffer.alloc(&e.driver, 4 * W * 4); | ||
| defer zero.free(); | ||
| try zero.fill8(0, null); | ||
| try t.c.decode(t.cs, zero.ptr, 4, t.audio_dev.ptr); |
There was a problem hiding this comment.
[P1] Keep the codec warmup inside the allocated audio buffer
--chunk 1 --first-chunk 1 reaches Tts.create with a one-frame maximum. The audio allocation is then 1 × 2048 × 4 = 8192 bytes, but this unconditional four-frame decode requires 32768 bytes under Codec.decode's output contract. Maxima 2 and 3 are also too small. The CPU transport fixture calls the actual create function and records this mismatch without running a GPU. The linked patch uses min(t.max_chunk, 4) for both the silent input allocation and decode call; all six bounds cases pass, and defaults at four or more retain their existing warmup.
| d3.wav.pcm16(x, b); | ||
| if (o.wav) |f| _ = fwrite(b.ptr, 1, b.len, f); | ||
| if (o.pcm) |f| { | ||
| _ = fwrite(b.ptr, 1, b.len, f); |
There was a problem hiding this comment.
[P2] Propagate audio write and finalization failures before reporting success
These fwrite and fflush results are discarded, so a full or broken output can still increase samples, let speak succeed and reach RESULT drama3 OK. A CPU test of the exact AudioOut block confirms both an immediate write failure and a buffered flush failure: ferror is set, yet put returns success and counts the two samples. The valid-sink control writes all four PCM bytes. Please check writes/flushes, and make final header seeking/writing/close failures observable before the success line too; current close silently discards them. The linked output diagnostic remains red and is not fixed by the warmup patch.
| } | ||
| } | ||
| if (a.codec_check) |ref| return checks.codecCheck(gpa, io, a, ref); | ||
| if (a.model.len == 0) { |
There was a problem hiding this comment.
[P2] Allow the documented codec-only encode mode past model admission
The usage advertises --codec CODEC --encode OUT --ref-audio REF.wav without --model, but this unconditional model-directory check exits with usage before the independent encode branch below. That branch only uses the codec and reference audio, not a.model. The actual CLI diagnostic reproduces exit 2 before any CUDA/codec access; its macOS link-only posix_fadvise trap shim is never called. Route encode before the model-only check, or exempt that mode from the model-directory requirement. This remains unfixed in the offered patches. They do not claim successful encoding.
|
Thank you, @cwschroeder: all three findings were right, and they're fixed in
The CI commands on the new head, Zig 0.17.0, run on a DGX Spark with the GPU hidden:
Thanks also to @BobClawblaw for the sm_89 contract run: all 45 Drama 3 launches pass gate 5 on the 4090. 🤖 Generated with Claude Code |
…inned Zig formatter The Metal backend's warmup had the same four-frame decode as CUDA's (ashhart#939 review by @cwschroeder). Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
cwschroeder
left a comment
There was a problem hiding this comment.
The three earlier findings are addressed at 3515c27f. I verified that the adopted warmup and formatting files match the offered patch. The six warmup cases pass in Debug and Fast. The actual CLI argument/admission prefix now admits codec-only encode without a model, with five other controls still passing. The unbuffered/buffered stdio-error cases now return AudioWrite, and successful PCM output still counts its samples.
There are two small cleanup notes at the newly fallible boundaries, below. An applicable two-line patch and CPU reproductions register error cleanup for the opened WAV and the returned host frames. Both cleanup cases fail on this head; all five output/ownership cases pass with the patch in Debug and Fast. These currently abort the CLI, so I am treating them as cleanup nits rather than a new generation or GPU failure.
At candidate 7b0f897c, fork validation #14 has all six current-head checks green, including the complete No-GPU commands, formatter and app/mobile workflows. Local formatting, whitespace, lean (517/517, zero new) and a compile-only Linux arm64 CLI build also pass; source review found no issue in the two-line delta. Your upstream workflows remain action_required, with no executed job results there. These CPU/CI receipts do not qualify model fidelity, codec output quality or GPU performance.
| o.wav = fopen(z.ptr, "wb") orelse return error.WavOpen; | ||
| var h: [44]u8 = undefined; | ||
| d3.wav.header(&h, 44100, 0x7FFFFFF0 / 2); | ||
| try writeAll(o.wav.?, &h); |
There was a problem hiding this comment.
[P3] Release the opened WAV if the initial header write fails
The new try writeAll can return before AudioOut.open transfers ownership to the caller. There is no cleanup registered here yet. A CPU fixture returns a real read-only FILE from the opening transport; the actual write returns AudioWrite, but F_GETFD shows its descriptor remains open. Add errdefer o.abandon() immediately after constructing o. This also covers the older later-open failure path.
| out.t0 = t_req; | ||
| errdefer out.abandon(); | ||
| const r = try t.speak(io, rows, q.smp, q.max_frames, .{ .first_chunk = a.first_chunk, .chunk = a.chunk }, .{ .ctx = &out, .audio = AudioOut.put }); | ||
| try out.close(); |
There was a problem hiding this comment.
[P3] Own the returned frames before fallible output finalization
speak has returned caller-owned r.frames, but the outer frame cleanup is registered only after this branch. If out.close() fails, those frames are lost. A CPU fixture closes the WAV descriptor after a stub speaker returns its host array; the actual finalization returns AudioWrite and DebugAllocator reports that array leaked. errdefer gpa.free(r.frames) immediately after speak covers the failure. It ends on successful branch exit, leaving the existing outer defer to free successful results.
|
@cwschroeder: both cleanup notes are in
At the new head, with the GPU hidden:
The Metal branch (#956) merges this commit. 🤖 Generated with Claude Code |
…rnels, codec, drama3-generate) zig/src/families/drama3: the Qwen3.5-MoE backbone (Gated DeltaNet and gated attention layers, 256 FP8 block experts top 8 with a gated shared expert) and the 13-layer codebook tower, decoding one frame of a main token and ten codes a step; prompt rows in the reference-bracket-v1 layout; the Fish S2 codec in fp32 for voice references (encode) and streamed audio (decode with carried state). zig/kernels/cuda/drama3*.cu: the glue, the samplers, the codec's kernels, and fp8_experts.cu's kernels on FP8 rows with per-row scales (the reference's dynamic activation scheme). cuBLAS (fp32 compute, no TF32) for prompt rows and the codec. zig build drama3-generate: text (and an optional voice reference) to WAV, the codec's encode, and --check / --codec-check against tools/drama3/oracle.py's dumps. tools/drama3: the PyTorch oracle (SGLang-Omni 416a70c0 semantics) and the one-time codec.pth conversion. Receipts are in the pull request.
…tes 1-4 registry.zig: every drama3, drama3_fp8 and drama3_codec launch with the records kernel-contract-limits reads from fatbins built for sm_80, sm_86, sm_89, sm_120 and sm_121. contract_drama3.zig: d3_rmsnorm's plain-gamma form against the rows family's float64 RMS norm.
Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
…ithout a model - The codec warmup decodes min(max_chunk, 4) frames, inside the audio buffer a short --chunk allocates. - Every WAV and PCM write, flush, header seek and close is checked; a failure ends the request before its RESULT line. - --codec CODEC --encode OUT --ref-audio REF.wav no longer needs --model, as the usage says. Reported in review by @cwschroeder; the warmup bound is his. Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
- AudioOut.open abandons what it opened if the first header write fails. - The frames speak returns are freed if finalizing the output fails. Reported in review by @cwschroeder. Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
75eeed7 to
048140e
Compare
…inned Zig formatter The Metal backend's warmup had the same four-frame decode as CUDA's (ashhart#939 review by @cwschroeder). Co-authored-by: Christian Schröder <1188270+cwschroeder@users.noreply.github.com>
|
Rebased on main |
…actly their length Engine.generate and Tts.speak returned host[0..made], a prefix of the limit-sized allocation, so a caller freeing the result after an early end of speech freed the wrong length. frames.zig's owned() shrinks the allocation to the frames kept (realloc), and endOfSpeech() is the scan both paths share. Host tests cover an early end, no end, and an end at the first frame, freed through the testing allocator.
Config.parse read .object, .array, .string and .bool straight off the parsed JSON, so a string where a number belongs, or an array where an object belongs, trapped instead of failing. Each field is now matched on its kind and refused with error.ConfigNotObject, ConfigNotInteger, ConfigNotArray, ConfigNotString or ConfigNotBool, naming the field in a warning. Token ids that do not fit int32 are error.BadConfig rather than a trapping cast, and the semantic band's width is computed in i64. Host tests feed the shipped config with one field changed for each kind.
|
Following up on the two points in the reply on #956 that apply to this branch. Both are fixed here; the head is now
Checks on a DGX Spark with the GPU hidden:
Kernels and launches are unchanged, so I didn't rerun the kernel contract. The prompt-chunk failure cases, reference margins and long/reset/concurrent audio cases are all kept. |
cwschroeder
left a comment
There was a problem hiding this comment.
Rechecked the shared output cleanup at 582e83a4. The exact AudioOut/frame-ownership blocks, WAV helper, frames helper and config parser are byte-identical to #956 at ec6f16dc, where I independently ran the five actual stdio/frame-cleanup cases and five frames/config cases in Debug and Fast. All pass. The two cleanup notes from my earlier review are closed.
The early-EOS regression also fails at the wrong-length free when only the old shortened-view return is restored in a temporary copy. Astra checked the owned-frame handoff, realloc failure cleanup and named config refusals. This confirms the shared host delta; it adds no CUDA device, codec/model fidelity or performance qualification. Current-head remote checks are absent.
|
Thanks @drowzeys for the exact-length EOS ownership fix and named config-kind refusals, with allocator and malformed-config controls. Those address the two requested boundaries and belong in the common CUDA/Metal code. We are carrying one Drama family with both contributors' work, the stronger codec, shared frame serving and completion-aware cleanup. The host tests and generator build do not settle the retained prompt-chunk failures, reference margins or long/reset/concurrent audio cases. Those remain gates on the combined source. The updated branch's unchanged kernels are not a reason to omit the final integrated numerical and resource checks. Posted by Ash's AI Assistant Synthena |
…tal, with #915-#926 and #939 beneath them #927's stack brings the process-wide cap on device and pinned host bytes, direct reads a file system refuses going through the page cache, Module.holds, the Drama 3 backbone, tower and speech codec on CUDA (codec.pth read without Python), speech requests and audio events in engine_api, POST /v1/audio/speech with streamed PCM, and its decode kernels; #956's (with #939) the family's shared parts, its Metal backend and drama3-generate on macOS. Laid as the maintained series: the bridges between the two, qualification, and owners that keep buffers and modules until their completion is proved. On this line the cap's reservations join the pinned peaks' accounting, the learner's API moves to core/learning_api.zig with engine_api.zig re-exporting it (Example keeps its context row), the CUDA family list holds LFM2 and fish_omni, and both build files keep this line's layout with Drama's steps added. The tests are named for what they check.
|
Thank you, @plotarmordev, for the speech protocol and infrastructure, and @drowzeys for the family, backends and codec. The selected work from #939 is included through #927 ( Posted by Ash's AI Assistant Synthena |
|
The consolidated Drama contribution shipped in 1.1.0 as Posted by Ash's AI Assistant Synthena |
Thank you, Ash, for the invitation to send this for review, and for the CUDA runtime it builds on.
This is the first speech model on TensorFold's Zig runtime. Fish Audio's Drama 3 is a 35B-A3B MoE backbone with a codebook tower and a neural codec. It now runs end to end on TensorFold's CUDA runtime, with no Python and no PyTorch at run time. Text, and optionally a voice to clone, go in; streamed audio comes out. This is the case for TensorFold beyond text: the same runtime, kernels contract and lean code can carry an audio model whose step is a frame rather than a token.
On one DGX Spark (GB10):
Precision is the checkpoint's own:
What comes next, for the gate we agree on:
u32token. How it fits the engine below lists the core changes. Plain decoding already satisfies "draft equals plain".I did not open an issue first, since this came by invitation. Happy to move the gate discussion into one if you prefer.
What it adds
These are new files, plus the minimal wiring (13 lines in
zig/build/cuda.zig, 3 inzig/src/cuda/kernels.zig, 7 in the contract runner, and 45 records inregistry.zig):zig/src/families/drama3/(Zig over the CUDA runtime; no Python and no PyTorch at run time):config.zigchecks every number the kernels assume and refuses any other checkpoint shape.weights.zigstacks the bf16 projections and repacks the FP8 experts.engine.zigandforward.zighold the prompt pass and the frame step. The step samples, runs the tower, records the frame and runs the next backbone row, all as one CUDA graph. The tower's codes 2-9 run on a second stream.prompt.zigbuilds prompt rows in SGLang-Omni's reference-bracket-v1 layout.codec.zigandcodec_encode.zigare the Fish S2 codec: a streamed causal decode with carried state, and the encoder for voice references.wav.zigholds the WAV I/O and torchaudio's resampler.tts.zigdecodes each finished burst of frames while the next burst generates.blas.zigopens cuBLAS's strided batched GEMM at run time.zig/kernels/cuda/drama3*.cu/.cuh:drama3.cuis one fatbin: the backbone and tower glue, decode and prompt attention, the router, and the samplers. The samplers implement the Philox exponential race of SGLang-Omni's Triton kernels, plus RAS.drama3_fp8.cuisfp8_experts.cuon FP8 rows with per-row scales.drama3_codec.cuholds the codec's row kernels between cuBLAS SGEMMs.zig build drama3-generate:--encodefor audio to codes;--check DUMPagainst the oracle's dumps: every layer probed, the free-running greedy divergence, and the oracle's frames teacher-forced, with every differing pick and its margin;--codec-check.tools/drama3/(development tools):oracle.pyis a standalone PyTorch reference following SGLang-Omni 416a70c0'sfishaudio_drama3.oracle_prompt.pyis a copy of itsprompt.py.codec_convert.pyis the one-time conversion ofcodec.pthinto the layoutcodec.zigreads.FISHCODEC_DIRpoints at them.THIRD_PARTY_NOTICES.md: a Drama 3 section.How it fits the engine, and how it does not
What a step decodes. A step decodes one frame: the main token, then codes 0-9.
Lanes. Drafted frames could be verified the way lanes verify tokens. The backbone would take k drafted rows in one forward, the tower would take each row, and the engine would accept the longest prefix whose whole frame equals plain decoding. With no drafter, "drafted equals plain" holds trivially. This branch decodes plain only, one request at a time.
What it would take to put Drama 3 on the lane core:
u32(an eleven-column frame), or a family hook that maps a frame to and from the core's tokens;Each of these is a core change, which is why I am asking before writing it. If you see a way to express a frame as lane tokens that I have missed, I will port to that instead.
Precision.
per_token_group_quant_fp8for this checkpoint'sactivation_scheme: dynamic. It keeps the reference's arithmetic: skipping it moves each MoE block's output by about 1e-2 relative.CUBLAS_COMPUTE_32F, default math, and no TF32.Receipts
Machine and settings for every run:
-Doptimize=ReleaseFast -Dsm=121e0a9218aa48888270f06ef1ed9ea67622c0e17d2(including itscodec.pth)Prompts (public):
Sampling: Fish's defaults (temperature 1.0, top-p 0.9, top-k 30), seed 1, at most 1,500 frames. Greedy runs are for parity only, because pure greedy loops.
Fidelity against the PyTorch oracle (
drama3-generate --check)The oracle is
tools/drama3/oracle.py: bf16 PyTorch, FP8 experts with dynamic activations, and fp32 where SGLang-Omni is fp32. It dumped two prompts greedily for 300 frames: the clone prompt, and a no-reference sentence ("Hello! This is the Wasserman Slate AI Studio, powered by TensorFold, built with integrated Fish Audio Drama 3.", 46 rows).Prompt pass, clone (168 rows):
dumps/fp32ref, same rows) shows these are ill-conditioned rows, not a port error. The oracle's own bf16 run is 0.959 on row 153 and 0.840 on row 149 against fp32. This port is 0.861 on row 153 against fp32: the expert tie above goes the other way.Teacher-forced, 300 frames each (my greedy pick against the oracle's at every column):
Every difference is a near tie on my side. Measured in bf16 ulps at the logit's magnitude, my pick's logit leads the oracle's pick by:
The 7-ulp case is a weak codebook head: 7.97 against 7.75. The run prints every difference.
The oracle dumps its logits only for frame 0's codebook heads and the candidate logits of its first 20 frames, so the two-sided tie policy can be checked only there. The first free-running divergence is frame 0, codebook head 7: my margin there is 0.0625 and the oracle's is 0.1875, between the same two codes (604 and 255).
Free-running greedy frames therefore differ from frame 0. Token-for-token greedy parity with the oracle does not hold. The teacher-forced numbers above are the fidelity claim.
Sampling: seeded runs cannot be compared with the oracle. The oracle draws with
torch.exponential_; this port implements the Philox stream of SGLang-Omni's Triton sampler, which I could not run here.End of speech, plain prompt, seeds 1-3:
Each side runs one seed to its cap with RAS repeatedly firing. This looks like the model's behaviour on this text, not the port's.
Codec (
--codec-check):Generation from the WAV reference gives the same frames as generation from the oracle's prompt rows: MD5
160c87ce….Determinism and request state
--max-rows 1024, every prompt up to 1,024 rows (about 44 s of reference audio plus the text) is one chunk. Longer prompts are not chunk-invariant. I have not fixed this.Performance: before and after on the same machine
drama3-cudaat 81173e6. That is the code this branch was ported from: built on an older base, with the experiments switched off by default.Memory:
Kernel contract
d3_rmsnorm, plain-gamma form, against the rows family's float64 RMS norm. It is the 13th kernel in the SUMMARY.tf_rms_rows_bf16.__CUDA_ARCH__ >= 900guard aroundgriddepcontrol.wait, and PDL is switched off below sm_90. Those images compile and fit, but have not run.Lean check and host tests
python3 tools/zig/lean_check.py: 517 problems on the base (46a08c1), 517 on this branch.tools/zig/lean_new.pyreports 0 new. All 517 are inherited.zig build test, fresh cachezig build test -Dcpu=apple_m1zig build test-cuda-host -Dcpu=apple_m1xcrunnot findingmetal, because that machine has no Metal toolchain. This branch touches no Metal code.Not tested, or not in this pull request
/v1/audio/speech, and no prompt cache or resume.--max-ctx 4096.Licenses
fishaudio/drama-3and itscodec.pthare under the Fish Audio Community License Agreement v1.0. That covers research and non-commercial use, and commercial use below a USD 500,000 annual revenue threshold. It grants no Hosted Service without a separate license from Fish Audio. Users download the checkpoint and accept that license themselves.codec_convert.pywrites is a derivative ofcodec.pthunder the same license.tools/drama3/oracle_prompt.pyis a copy of itsprompt.py.codec.ziganddrama3_codec.cuare separate implementations and include none of that source. The oracle imports those modules from a directory the user supplies.Reproducing
🤖 Generated with Claude Code
https://claude.ai/code/session_01PfjmufXm4L7qsVNFyRw9ZK