Skip to content

drama3: Fish Audio Drama 3 text to speech on Metal, from Zig (stacked on #939) - #956

Closed
drowzeys wants to merge 15 commits into
ashhart:mainfrom
drowzeys:upstream/drama3-metal
Closed

drowzeys wants to merge 15 commits into
ashhart:mainfrom
drowzeys:upstream/drama3-metal

Conversation

@drowzeys

Copy link
Copy Markdown
Contributor

This puts the first speech model on TensorFold's own Metal runtime. Fish Audio's Drama 3 (a 35B-A3B MoE backbone with Gated DeltaNet, a 13-layer codebook tower and a neural codec) now runs end to end on Apple Silicon from Zig and Metal. There is no MLX, no Python and no PyTorch at run time. Text, and optionally a voice to clone, go in; streamed audio comes out. It is the Metal half of #939 and shares that pull request's config, prompt, checkpoint layout, WAV code and check tools. TensorFold could already run text models on both backends; with this, an audio model whose step is a frame of eleven columns runs on both too, on the same runtimes, kernel contract and lean code as the video work.

On one Mac Studio (M5 Ultra, 256 GB):

  • 84 frames/s with audio out, 3.9× real time. The MLX engine on the same machine, prompts and seed makes 70 (runs alternated, three each). With a voice clone: 82 against 72.
  • First audio in 0.14–0.15 s. With a voice clone it is 0.41 s, which includes encoding the reference WAV (76 ms).
  • 9.3–9.7 GB peak process footprint. The 32.2 GB of FP8 experts are read in place from the checkpoint's mapped shards. The MLX engine's peak footprint is 67.7 GB.
  • Repeats are byte-identical. Three processes, and three requests in one process, give the same frames and WAVs.
  • Teacher-forced agreement with the PyTorch oracle is 98.53 % and 97.57 %, measured with drama3: Fish Audio Drama 3 text to speech on one CUDA GPU (new family, plain decoding; proposal) #939's --check. Every differing pick is listed. The largest margin is 0.25, the same as on CUDA, where the figures are 98.80 % and 97.97 %.
  • Codec decode SNR 113 dB against the PyTorch codec (CUDA: 115). The encoder's codes match 850 of 850.
  • Kernel contract: all 38 new Metal launches pass gate 5, and d3_rmsnorm passes gates 1–4.
  • Lean check: 0 new findings. zig build test passes every step on macOS.

Precision is the checkpoint's own, as on CUDA:

  • bf16 activations.
  • The FP8 experts as the reference serves them. Metal has no FP8 type, so each e4m3 weight is decoded exactly to fp32 inside the kernel. The activations are rounded to e4m3 per row and per 128 inputs (SGLang's per_token_group_quant_fp8), and the checkpoint's block scales are applied per 128-input group.
  • fp32 for norms, softmax, rotary, the DeltaNet state, attention probabilities and the whole codec. drama3.metallib is built with safe math and precise fp32 functions.

Next steps:

What it adds

On top of #939, in two commits:

  1. drama3: share the backend-independent parts before a Metal backend. No change to what the CUDA engine computes.
    • common.zig holds the request settings, the parity probe, the streaming stats and the clock.
    • shards.zig holds the checkpoint's shards by tensor name and lm_head's candidate ids.
    • check.zig and generate.zig no longer touch the CUDA runtime. The engine gives them scratch bytes and its peak allocation, and tts.CodecOnly opens the codec alone. The same two files now drive both backends.
  2. drama3: Fish Audio Drama 3 text to speech on Metal:
    • zig/kernels/metal/drama3/ (one metallib, built ahead of time):
      • dense.metal holds the bf16 projection rows, with 4 columns a simdgroup and one fma chain a lane, so a row's bits do not depend on how many rows run. It also holds the zero-centred norms, the embeddings, SwiGLU, the MoE combine and the tower's input mix.
      • gdn.metal holds the causal conv and the gated delta rule in fp32.
      • attn.metal holds the q / k norms with rotary, and gated attention for prompt and decode rows alike.
      • moe.metal holds the router, the FP8 activation rows, the prompt's routing plan, and the FP8 block-scaled experts read through per-layer pointer tables into the mapped shards.
      • sample.metal holds the greedy picks, the Philox race with RAS (drama3: Fish Audio Drama 3 text to speech on one CUDA GPU (new family, plain decoding; proposal) #939's fast path), and the frame's bookkeeping.
      • codec.metal holds the codec's fp32 GEMM (a causal conv's taps in one pass) and its glue.
    • zig/src/families/drama3/metal/: gpu.zig (the queue, the pipelines, and buffers addressed as u64, so the shared tools work unchanged), weights.zig, engine.zig, forward.zig, kernels.zig, codec.zig, codec_encode.zig and tts.zig. drama3_metal.zig is the family's Metal root.
    • zig/build/drama3_metal.zig, plus 4 lines in build.zig: the metallib, the family module, and zig build drama3-generate on macOS.
    • The kernel contract: 38 records in registry.zig; zig/tests/contract/metal_drama3.zig (gates 1–4 for d3_rmsnorm, and the drama3 sources for gate 5); a few lines in the runner; and the sources in zig/kernels/metal/sources.zig.

Each frame is encoded straight into a serial compute encoder and committed as soon as it is encoded, so the next frame encodes while the GPU runs the last one. Device scalars (position, step, counts) advance on the GPU, as on CUDA, so a burst of frames needs no host round trip.

Receipts

Machine and settings for every run:

Machine Mac Studio, Apple M5 Ultra, 256 GB unified memory
OS macOS 27.0.1, Xcode 27.1, Metal toolchain 27A266a (-std=metal4.1)
Build Zig 0.17.0, zig build drama3-generate (ReleaseFast)
Checkpoint fishaudio/drama-3 at revision e0a9218aa48888270f06ef1ed9ea67622c0e17d2 (including its codec.pth)
Process one model process at a time, under a lock shared with the other GPU jobs on the machine and a memory guard

Another process on the machine: during the timed runs an Ollama server, which is not part of this work, held a 35B model in memory. I did not measure whether it was busy.

The prompts are #939's public ones:

  • "plain" is Harvard sentences list 1, sentences 1-2.
  • "clone" has the Common Voice en clip 10119832 (CC0) as its voice reference: 85 frames, 168 prompt rows.

Sampling uses Fish's defaults (temperature 1.0, top-p 0.9, top-k 30) with seed 1.

Fidelity against the PyTorch oracle (drama3-generate --check, #939's dumps)

Teacher-forced, 300 frames each. My greedy pick is compared with the oracle's at every column.

prompt picks equal differences my largest margin over the oracle's pick CUDA (#939)
clone 98.53 % of 3,000 44 0.25 98.80 %, 36
no-reference 97.57 % of 3,000 73 0.25 97.97 %, 61
  • Every difference is a near tie on my side. The run prints each one with its margin: some are exact ties (0), the rest 0.0625 to 0.25 on logits of 10 to 30.
  • Decode rows: the final-norm hidden of the decode rows is at worst cos 0.9961 (clone) and 0.9966 (no-reference) over the oracle's dumped rows. CUDA's are 0.9957 and 0.9977.

Prompt pass, clone (168 rows):

Greedy: as on CUDA, free-running greedy frames differ from frame 0 on a near tie (frame 0, codebook head 0: a margin of 0.125 on both sides). Token-for-token greedy parity does not hold; the teacher-forced numbers above are the fidelity claim.

Intelligibility: Whisper large-v3-turbo transcribes the plain run's audio as "The birch canoe slid on the smooth planks. Glue the sheet to the dark blue background." It transcribes the clone run's as "Fine. If we miss it, it's on you. Well, maybe that's fair." Whisper does not transcribe the bracketed directions.

Codec (drama3-generate --codec-check)

check result
Streamed decode of 300 frames (a first chunk of 4, then 32) against the PyTorch codec in fp32 SNR 113.16 dB
Resampler, 24 kHz to 44.1 kHz, against torchaudio 144.00 dB
Encoder blocks 99.3–138.4 dB
Reference codes 850 of 850 equal (100 %)
Decode speed, 300 frames 54x real time (first chunk of 4 in 11.6 ms)
Encode, 85 frames 76–180 ms

Performance: this port against the MLX engine on the same machine

The MLX engine is the fork's drama3-mlx branch in its own venv. It quantizes the experts to 8 bits, where this port serves the checkpoint's FP8. The two therefore make different frames from the same seed, and each run is timed over its own frames. Each was run with streaming on (--stream for MLX; this port always streams) and without its own speech check (--no-verify for MLX). The runs alternated (Metal, MLX, Metal, MLX, ...), three each.

run engine frames frames/s first audio ms prefill ms peak footprint
plain this port 178 (ends) 84.20 / 84.31 / 84.53 143 / 150 / 146 90 / 97 / 93 9.3 GB
plain MLX 451 (ends) 70.32 / 70.29 / 70.40 229 / 253 / 233 80 / 97 / 82 67.7 GB
clone from WAV this port 299 (ends) 81.75 / 82.14 / 81.91 408 / 407 / 407 278 / 278 / 277 9.7 GB
clone from WAV MLX 125 (ends) 71.54 / 72.44 / 71.86 269 / 260 / 272 119 / 111 / 117 67.7 GB
  • Frames/s here includes the codec, since this port decodes each burst on the same queue. With no codec (--codec left out), the plain run makes 91 frames/s.
  • First audio for the clone run includes encoding the reference WAV (76 ms) and the 168-row prompt.
  • Load takes 7.4 s: 6.36 GB of bf16 weights are copied, and the experts are mapped, not copied.
  • Peak footprint is the process's own (time -l). The experts' 32.2 GB are file pages read in place, so they are not in that figure. The runtime's own count of Metal buffers is 48.5–48.9 GB, because it includes whole mapped shards. The MLX engine's figure is its own peak footprint.
  • Standard server and the PyTorch reference not run on this machine: the PyTorch oracle needs CUDA here, and SGLang-Omni has no Metal backend.

Determinism and request state

  • Repeats: three processes give byte-identical frames and WAVs. Plain frames are MD5 f5d12873…; clone frames are 868b41e6….
  • Reset: three requests in one process give the frames of a fresh process.
  • Prompt chunks: dense rows, the experts and attention compute each row on its own, so a row's bits do not depend on the chunk it is in. Chunked prompts beyond --max-rows were not tested.

Kernel contract (M5 Ultra)

SUMMARY 27 kernels: 27 pass gates 1-4, 0 fail, 2 known findings; 5 controls: 5 red as planted, 0 missed; 0 limit problems; gates 106204 ms, limits 55 ms
PASS limits: 115 registered Metal launches against M1-M5, 0 problems
  • Gates 1–4: d3_rmsnorm, plain-gamma form, against the rows family's float64 RMS norm, as on CUDA.
    • nonfinite: 2,052 non-finite outputs match their classes.
    • repeat: byte-identical.
    • shapes: worst 0.996 of the bound, with 4 edges refused.
    • sums: within the bound at k = 2,048.
  • Gate 5: all 38 Drama 3 Metal launches.
    • Each kernel reserves its threads in the source (max_total_threads_per_threadgroup), so none needs an M1/M2 compile record.
    • All 38 pass the on-GPU check of their pipelines and threadgroup bytes.
  • The two known findings are affine_mm's, from before this pull request.
  • Not run: M1–M4. Gate 5 checks the declarations against them, but the kernels have not been run there.

Lean check and tests

run steps tests passed skipped failed
macOS (M5 Ultra), zig build test -Dcpu=apple_m1, fresh cache, on #939 206/206 2,995 of 3,005 10 0
macOS (M5 Ultra), zig build test -Dcpu=apple_m1, fresh cache, this branch 216/216 3,002 of 3,012 10 0

Not tested, or not in this pull request

Licenses

As in #939:

Reproducing

# macOS with Xcode's Metal toolchain (xcodebuild -downloadComponent MetalToolchain)
zig build drama3-generate
# once, with PyTorch: codec.pth -> the native codec's file (#939)
python3 tools/drama3/codec_convert.py /path/to/drama-3 codec-v1.safetensors
zig-out/bin/drama3-generate --model /path/to/drama-3 --codec codec-v1.safetensors \
  --text "The birch canoe slid on the smooth planks. Glue the sheet to the dark blue background." --seed 1 --wav out.wav
# parity against #939's oracle dumps, and the codec
zig-out/bin/drama3-generate --model /path/to/drama-3 --check DUMP --check-frames 60
zig-out/bin/drama3-generate --codec-check REF --codec codec-v1.safetensors --frames FRAMES --ref-audio REF.wav
# kernel contract
zig build tf-kernel-contract && zig-out/bin/tf-kernel-contract

This branch's own commits: drowzeys/TensorFold@upstream/drama3-cuda...upstream/drama3-metal

🤖 Generated with Claude Code

https://claude.ai/code/session_01PfjmufXm4L7qsVNFyRw9ZK

The review fixes on #939 (warmup bound, checked audio writes, encode without a model) are merged into this branch, and the same warmup bound is applied to the Metal codec. After them, on the M5 Ultra: zig build test -Dcpu=apple_m1 passes 216/216 steps, 3,002 of 3,012 tests (10 skipped), and zig fmt --check build.zig zig is clean.

The Studio that runs Drama 3 is #941, and its Voice tab is #944.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PfjmufXm4L7qsVNFyRw9ZK

@cwschroeder cwschroeder left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Host control-flow/ownership review of c6af3262, limited to the new Metal wrapper and its buffer callers. The three earlier #939 fixes are present, including the Metal warmup bound. The shared AudioOut and returned-frame ownership blocks are byte-identical to #939's 3515c27f; the two-line cleanup patch already offered there still applies unchanged.

Two new error-path findings are below. Separate patches and CPU reproducers fix them at 85a5fdba + 5fd8de65. The actual complete Gpu wrapper runs over inert Metal objects, with the unchanged mapShards function body and real allocator-failure injection. On this head, three controls pass and nine assertions fail; all twelve cases pass with the patches in Debug and Fast. The fixture checks direct close and two failed commands, not just a second cleanup call after a failed commit. Source review found no remaining issue in this delta.

Whole-tree format, whitespace and lean pass (517/517, zero new). The complete Metal CLI compiles and links locally with Zig 0.17.0 and Xcode 26.6, without execution. Fork validation #15 has all six current-head checks green on the exact candidate, including all four No GPU commands, formatter and app/mobile workflows. The upstream No GPU run 38917505503 is action_required with zero jobs. These checks do not verify Metal device execution, model/codec fidelity or the performance receipts.

Comment thread zig/src/families/drama3/metal/gpu.zig Outdated
cb.commit();
g.enc = null;
g.cb = null;
try g.waitPending();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Drain the new command before returning the previous command's failure

The new command is already committed and explicitly retained above. If waiting for the previous pending command reports CommandBufferFailed, this try returns before storing the new command or popping its pool. waitPending has cleared the old pending pointer, so a direct close() no longer waits for the new work before releasing buffers. Engine.destroy also relies on its initial flush before unmapping the shards.

The CPU transport reproducer sees resource teardown before the new command's wait. The patch waits/releases the newly submitted command on this error path and always pops the pool, while preserving the original error and the success-path pending reference.

Comment thread zig/src/families/drama3/metal/gpu.zig Outdated
/// An address for a buffer made elsewhere (a mapped checkpoint shard); `own` releases it at close.
pub fn register(g: *Gpu, b: mtl.Buffer, own: bool) !u64 {
try g.bufs.append(g.gpa, b);
try g.owned.append(g.gpa, own);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make registration atomic before transferring buffer ownership

If bufs.append grows successfully and this second append fails, the lists have different lengths. close() later iterates them as equal-length slices, so allocation failure leaves the wrapper unable to tear down normally. The creator also needs to release a newly created buffer if registration fails: this applies to alloc and the mapped-shard caller.

The offered second patch reserves both capacities before appending either entry, adds creator-side buffer cleanup on failed handoff, and frees mapShards' temporary address vector on error. The later indirect.append failure keeps ownership with Gpu, avoiding a double release. Real FailingAllocator cases reproduce both list-growth and caller-cleanup failures.

@drowzeys

Copy link
Copy Markdown
Contributor Author

Thank you, @cwschroeder. Both findings were right, and they're fixed in fb748fa3. The head is d88f709e, which also merges #939's cleanup.

  • [P2] Draining the new command:
    • If waiting for the previous command fails, commit now waits for and releases the command it just queued, then returns the original error.
    • The pool is popped on every path, from a defer.
    • The success path still keeps the new command as pending.
  • [P2] Atomic registration:
    • register reserves capacity in both bufs and owned before appending either entry, so a failed allocation leaves them the same length and the caller still owning the buffer.
    • alloc and mapShards release a buffer whose registration failed.
    • mapShards frees its address vector on error.
    • Once a buffer is registered, a later indirect.append failure leaves it with the Gpu, so nothing is released twice.

On the M5 Ultra at d88f709e:

  • zig build test -Dcpu=apple_m1 passes 216/216 steps.
  • zig fmt --check is clean, and lean is 517 against 517.
  • Two device runs of the plain prompt (seed 1, ReleaseFast) each gave 178 frames at 84.3–84.4 frames/s, with first audio at 135–160 ms. The two WAVs are byte-identical (sha256 615bf9fd…), matching the receipts in the body.

🤖 Generated with Claude Code

@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thank you, @drowzeys, sharing the CUDA/Metal family structure and FP32 codec gives the merged Drama work a useful model baseline.

We checked d88f709 and #939. Both generation paths return an EOS-shortened slice of a larger allocation; we will preserve the full owner or shrink it and add early-EOS cleanup tests. The shared config also needs named refusals for wrong JSON kinds. Metal already refuses unsupported sampled top-k, and that guard stays.

We will combine this model with @plotarmordev's protocol/PTH and optimization work, then the common frame scheduler and native speech adapter. Please keep prompt-chunk failures, two-sided reference margins and long/reset/concurrent audio cases visible for the final fidelity gate. Current resource checks are separate from all-kernel numerical qualification.

Posted by Ash's AI Assistant Synthena

@ashhart ashhart mentioned this pull request Oct 17, 2026
drowzeys and others added 11 commits October 16, 2026 21:29
common.zig: the request settings, the parity probe, the streaming stats and the clock. shards.zig: the
checkpoint's shards by tensor name and lm_head's candidate ids. check.zig and generate.zig no longer touch
the CUDA runtime: the engine gives them scratch bytes and its peak allocation, and tts.CodecOnly opens the
codec alone for --encode and --codec-check. No change to what the CUDA engine computes.
…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.
…codec, drama3-generate on macOS)

zig/kernels/metal/drama3: the backbone and tower glue, bf16 projection rows (one fma chain a lane, so a row's
bits do not depend on the batch), Gated DeltaNet in fp32, gated attention, the router, FP8 activation rows,
the FP8 block-scaled experts read in place from the checkpoint (e4m3 decoded exactly, the reference's group
scales), the Philox samplers with RAS, and the codec's fp32 GEMM and glue. drama3.metallib is built ahead of
time with safe math and precise fp32 functions.

zig/src/families/drama3/metal: the same family over TensorFold's Metal runtime, behind the API drama3-generate
and its --check / --codec-check already use, so parity is measured the same way as on CUDA. Every kernel
reserves its threads; gate 5 holds all 38 launches and d3_rmsnorm passes gates 1-4. 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>
…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>
…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>
…uffers atomically

- commit waits for and releases the command it just queued when waiting for the previous one fails, and always pops
  the pool, so close() cannot release buffers under queued work.
- register grows both slot lists before appending either; alloc and mapShards release a buffer whose registration
  failed, and mapShards frees its address vector on error.

Reported in review by @cwschroeder.

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>
@drowzeys
drowzeys force-pushed the upstream/drama3-metal branch from d88f709 to b8e01ee Compare October 17, 2026 01:30
@drowzeys

Copy link
Copy Markdown
Contributor Author

Rebased on main 6ee828c2 together with #939; head d88f709e is now b8e01ee3. The pull request's diff against main is patch-identical to before (main's new commits only touch docs); zig fmt --check clean on the changed Zig files; python3 tools/zig/lean_check.py reports no new findings against 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.
# Conflicts:
#	zig/src/families/drama3/drama3.zig
#	zig/src/families/drama3/engine.zig
#	zig/src/families/drama3/tts.zig
… of exactly their length

The Metal Engine.generate and Tts.speak returned host[0..made] of the
limit-sized allocation, as the CUDA paths did. They now use frames.zig from
the merged CUDA fix: endOfSpeech() for the scan and owned() to shrink the
allocation to the frames kept, so drama3-generate frees the length it holds.
frames.zig's host tests run in the Metal family's tests too.
@drowzeys

Copy link
Copy Markdown
Contributor Author

Thank you. Both points are fixed; the head is now ec6f16dc. The shared parts were fixed on #939 and merged in here, the same way the branch already merges drama3-cuda (merge commit a1b40cd1, no rebase).

Metal's guard that refuses unsupported sampled top-k is unchanged.

Checks on the M5 Ultra:

Check Result
zig fmt --check build.zig zig clean
lean_check 517, the same as main
zig build test -Dcpu=apple_m1 216/216 steps
drama3-generate builds

These are build and host tests only; I didn't do a device run. The prompt-chunk failure cases, reference margins and long/reset/concurrent audio cases are all kept.

@cwschroeder cwschroeder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped host-ownership re-review of ec6f16dc on main 6ee828c2: the two P2 findings in my earlier review are closed. Astra independently found no material regression in the adopted cleanup or the new EOS/config delta.

The published CPU fixture runs the complete unchanged Gpu source and the actual mapShards body over inert Metal objects. All 12 cases pass in Debug and Fast at this head. Running the same fixture against original c6af3262 still gives 3 pass / 9 fail, covering the missing drain, pool cleanup and allocation ownership. The five actual stdio/frame-cleanup cases also pass in both modes. No device is opened.

The shared frames/config tests pass 5/5 in Debug and Fast, including early EOS and named wrong-kind refusals. Restoring only the old shortened-view return in a temporary copy makes the early-EOS test abort at the wrong-length free. Source review confirms all four CUDA/Metal generate/speak callers use the owned result and retain cleanup on realloc failure.

Whole-tree format and patch whitespace pass; lean is 517 on both pinned main and this head, zero new. Current-head remote checks are absent. This approval covers the host ownership changes reviewed here; it does not qualify device execution, model/audio fidelity, the remaining kernel gates, scheduling, or performance. No deployment or model run was performed.

@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thanks @drowzeys for carrying the shared config/EOS fixes from #939 and fixing both Metal return paths too. Freeing the exact owned length after early EOS, including zero frames, is the important ownership control.

We are keeping CUDA and Metal behind one Drama family and shared frame lifecycle, with both contributors credited. The unsupported sampled-top-k refusal remains explicit. The host checks do not establish prompt/decode fidelity, chunk invariance or long/reset/concurrent audio behavior on the merged backend. Those numerical, resource and served checks remain required, rather than inferred from the generator compiling.

Posted by Ash's AI Assistant Synthena

ashhart added a commit that referenced this pull request Oct 18, 2026
…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.
ashhart added a commit that referenced this pull request Oct 18, 2026
#956 registered d3_rmsnorm under gates 1-4 and its 45 CUDA launches under gate 5, and the Metal kernels with their
sources; the maintained series kept the adapters (contract_drama3.zig, metal_drama3.zig) but not their wiring. The
CUDA runner maps the drama3, drama3_fp8 and drama3_codec images and runs d3_rmsnorm's gates (gate 5 failed on
d3_gemv's missing image), and the Metal contract takes Drama's norm, its sources and its launches' limits.
ashhart added a commit that referenced this pull request Oct 18, 2026
tools/drama3/oracle_prompt.py is a copy of SGLang-Omni's prompt.py, and the oracle, its model half and the codec
converter import it; code copied from another project does not land, so the four scripts stay on #927's and #956's
branches with the 25 archived #927 diagnostic programs (oracles_927/*.zig.txt, whose originals are in #927's merged
history) and the series' provenance note. Serving reads the checkpoint and codec.pth in Zig, so nothing a user runs
changes; the notices and the codec's comments no longer name the scripts.
@ashhart

ashhart commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thank you, @plotarmordev, for the speech protocol and infrastructure, and @drowzeys for the family, backends and codec. The selected work from #956 is included through #927 (5dcf77d7c), the consolidated Drama 3 landing in 1.1.0. One CUDA/Metal family uses bounded shared frame rounds, request-owned state, native checkpoint/codec loading and streamed PCM speech. HTTP retains its checked default-voice subset. Full scope and audio, interleaving and completion results are on #927: Apple M5 Ultra norm checks passed 1/1 and CUDA resources passed; checkpoint audio fidelity, seeded concurrency and cancellation remain unrun.

Posted by Ash's AI Assistant Synthena

@ashhart

ashhart commented Oct 19, 2026

Copy link
Copy Markdown
Owner

The consolidated Drama contribution shipped in 1.1.0 as 5dcf77d7c; thanks @plotarmordev and @drowzeys. Experimental attention variants remain separate. We're closing it as landed.

Posted by Ash's AI Assistant Synthena

@ashhart ashhart closed this Oct 19, 2026
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…on CUDA and Metal, with ashhart#915-ashhart#926 and ashhart#939 beneath them

ashhart#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; ashhart#956's (with ashhart#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.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
ashhart#956 registered d3_rmsnorm under gates 1-4 and its 45 CUDA launches under gate 5, and the Metal kernels with their
sources; the maintained series kept the adapters (contract_drama3.zig, metal_drama3.zig) but not their wiring. The
CUDA runner maps the drama3, drama3_fp8 and drama3_codec images and runs d3_rmsnorm's gates (gate 5 failed on
d3_gemv's missing image), and the Metal contract takes Drama's norm, its sources and its launches' limits.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
tools/drama3/oracle_prompt.py is a copy of SGLang-Omni's prompt.py, and the oracle, its model half and the codec
converter import it; code copied from another project does not land, so the four scripts stay on ashhart#927's and ashhart#956's
branches with the 25 archived ashhart#927 diagnostic programs (oracles_927/*.zig.txt, whose originals are in ashhart#927's merged
history) and the series' provenance note. Serving reads the checkpoint and codec.pth in Zig, so nothing a user runs
changes; the notices and the codec's comments no longer name the scripts.
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.

3 participants