Skip to content

qwen3-asr: host slice, the shared tower and Qwen3 schedules over a backend binding (#2105) - #2239

Draft
jschmied wants to merge 24 commits into
ashhart:mainfrom
jschmied:qwen3-asr
Draft

jschmied wants to merge 24 commits into
ashhart:mainfrom
jschmied:qwen3-asr

Conversation

@jschmied

@jschmied jschmied commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Refs #2105. Rebased on main 000b534a2 (with #2285); head 2fe5b72d9. Draft: one gate step is open (below).

On top of 51dc03072, 89d85b860 re-pins three rows in gradient-serving.tsv and 2fe5b72d9 adds the CHANGELOG
line. Every run and check below was made on 51dc03072; the code they cover is the same.

The first slice of #2105: the family's host parts, a tower schedule shared by every backend, a Qwen3 decoder schedule
for the host reference and the Metal and XPU bindings, and the CUDA path, measured on a GB10. It doesn't touch the
server and doesn't depend on #1632.

It reads the checkpoint's Qwen2 slow-tokenizer export through #2225, now on main, because the checkpoint ships no
tokenizer.json.

Shape

  • families/qwen3_asr/ (host, any OS):
    • config admission (0.6B and 1.7B) and the tensor map by name;
    • the log-mel rows, PCM decode, and the chunk and window plan;
    • a float64 tower reference;
    • tower.zig and lm.zig: the schedules, generic over a binding r. They never import a backend.
  • schedule_test.zig runs both schedules on a host binding. The tower must match the float64 reference at torch's
    bf16 points (global attention fails it), and every LM op must stay inside its buffers.
  • families/qwen3_asr/cuda/: a device store, the tower over vit.cu's LayerNorm, pointwise and cuBLASLt Linear
    kernels, and model.zig, which runs the language model through qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285's Qwen3 pass (next section).

One Qwen3 layer path on CUDA: #2285's pass

  • qwen3/cuda/lm.zig gains forwardFrom(…, Extra) (:343). Extra (:200) holds:
    • start: the input's first position; the attention reads its positions from a.pos + 4 * (start + p) (:457);
    • cache: every layer's own k and v caches in place of the scratch's one (:445);
    • splice: rows copied over the embedded ones, cut to the pass (:348);
    • head: the final norm over the pass's last rows and the tied head on the device, as one more pinned (vocab,
      hidden) plan that only ASR prepares (:363, qwen3_asr/cuda/model.zig:91);
    • tap: stage reads for tf-qwen3-asr's stage compare.
  • Each field is read only when set. forward is forwardFrom(.{}): the same launches with the same arguments, and
    the embedding engine's prepare plan list is unchanged.
  • lm.admit (:210) refuses a start or head on more than one input or without its caches, a head over no rows or
    more rows than the pass, and positions past the caches, before any launch. Its test (:218) names each refusal;
    with the cache check planted out it goes red (18/19), and green again (19/19) with it back.
  • Shared, not copied: weights.Model.fromCheckpoint (weights.zig:46) loads the ASR checkpoint's Qwen3 under its
    thinker.model. scope. The position and RoPE uploads moved from the engine into lm.tables (lm.zig:127), which
    both call (engine.zig:75). qwen3/pass_cuda.zig is the small module root the ASR tools import.
  • Gone: qwen3_asr/cuda/lm.zig (our binding of the schedule on CUDA) and tf-qwen3-asr-forced --row-gemms.
  • ASR now uses qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285's numeric points: fp32 norm weights and its fp32 RoPE table. Before, it used bf16-rounded ones.
  • Arminova's Part B (rerankers), which adds a row-start offset to attention, isn't open yet. So Extra.start is
    the start for now; when B lands I'll move it onto B's offset.
  • The host schedule (qwen3_asr/lm.zig) stays in the family for now. It moves to families/qwen3/schedule.zig with
    the Metal and XPU bindings (qwen3-asr: the Metal binding (#2105, stacked on #2239) #2254, qwen3-asr: the XPU binding on Gen12LP (#2105, stacked on #2239) #2255), its only other users.

Provenance

The family was written from TensorFold's code, the checkpoint's config files, and the publisher's reference package
qwen-asr 0.0.6. From that package I read the transformers-backend modeling and processor, the inference wrapper, and a
search of its vLLM-backend file for the attention windows. The log-mel front end was checked against transformers'
WhisperFeatureExtractor source, which the package calls. No code was copied from any of them.

Gate: the tie rule (as on #2648), teacher-forced against qwen-asr 0.0.6, on 51dc03072

  • Reference: the transformers backend with the vLLM backend's attention windows applied.
  • Clips: the 40 public gate clips (LibriSpeech test-clean 20, FLEURS cmn_hans_cn test 20, both CC BY 4.0), a 50-
    and a 99-frame cut, and 3 LibriSpeech dev clips. That's 45 per checkpoint.
  • tf-qwen3-asr-forced runs each clip on the reference's greedy ids to the end. A step whose top token differs
    passes only if the two tokens swap places and both margins are at most 0.25 logit. Every such step is listed, and
    anything else fails with exit 1.
  • 1.7B: 45 of 45 pass. Swaps: 1272-128104-0004 step 11 (ours 3138 at 0.25, the reference's 705 at 0.000),
    cut99 step 7 (13 at 0.000, 30 at 0.125), cmn-10369927130798325266 step 19 (3837 at 0.000, 67338 at 0.25),
    cmn-15324743446256875074 step 23 (17 at 0.25, 40820 at 0.25).
  • 0.6B: 43 of 45 pass, one listed exception, one new fail.
    • Swaps: 1272-128104-0004 step 11 (705 at 0.125, 11 at 0.000); cmn-10104380129516909856 step 27 (3837 at
      0.25, 1773 at 0.000).

    • Listed exception (agreed on review, unchanged): cmn-15324743446256875074 step 10. Ours is 468 (margin 0.125).
      The bf16 reference picks 9197 (margin 0.25), with 468 third. The package's fp32 run picks 468, at 25.7277 against
      9197's 25.7162.

    • Open fail, cut50 step 3: ours is 5632 with margin 0.625 over 18284. The reference case picks 18284 at
      0.125. The package's own teacher-forced logits on this clip disagree with that case: its bf16 run puts 5632 first
      (22.75 against 22.625) and its fp32 run puts 18284 first (22.7922 against 22.5113). With the bf16-rounded norms
      (r4) this step was a swap at 0.25; qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285's fp32 norms move it to 0.625.

    • The rule for listing a step (review, 6826729645): the package's bf16 and fp32 runs order the two tokens oppositely,
      and either our pick is the fp32 pick or our fp32 distance on that clip is no worse than the package's bf16. Step
      10 meets it. cut50 step 3 meets the first part, and the whole clip is closer to fp32 than bf16 is, but step 3
      alone is not (0.667 / 0.1191 against 0.536 / 0.0843, table below; our worst column there is 18284 itself). So it
      is not listed, and this stays draft.

    • Stage compare at step 3 against the package's fp32 run: the tower is level with bf16. Through LM layer 20 our
      mean is at or below bf16's. From lm_layer21 both columns are above it, and 18284's lead (a logit lens through
      the final norm and head rows) falls further behind fp32 over layers 21-27. At the head the lead is -0.625 for
      ours, -0.125 for bf16 and +0.281 for fp32: the gap builds up, with no single stage out of line.
      The prompt is extended by the forced ids of steps 0-2. LM rows are at step 3's row; worst column / mean |diff|
      to fp32; the lead is 18284 minus 5632 through the lens:

      stage ours vs fp32 bf16 vs fp32 lead: ours / bf16 / fp32
      lm_layer17 0.073 / 0.0135 0.089 / 0.0184 -0.053 / +0.024 / +0.130
      lm_layer18 0.088 / 0.0184 0.211 / 0.0266 +2.293 / +2.437 / +2.474
      lm_layer19 0.152 / 0.0268 0.266 / 0.0343 +2.940 / +3.208 / +3.294
      lm_layer20 0.298 / 0.0451 0.327 / 0.0485 +3.459 / +3.627 / +3.863
      lm_layer21 0.860 / 0.0750 0.431 / 0.0693 +1.224 / +1.509 / +1.843
      lm_layer22 0.655 / 0.0973 0.655 / 0.0859 +2.447 / +2.654 / +2.979
      lm_layer23 0.748 / 0.1380 0.982 / 0.1084 +4.229 / +4.562 / +5.006
      lm_layer24 0.954 / 0.1681 1.353 / 0.1325 +3.658 / +3.947 / +4.361
      lm_layer25 0.993 / 0.2126 1.489 / 0.1653 +3.348 / +3.958 / +4.282
      lm_layer26 1.050 / 0.2264 1.173 / 0.1923 -0.188 / +0.322 / +0.629
      lm_layer27 1.571 / 0.2507 1.194 / 0.2238 -0.637 / -0.045 / +0.281
      lm_norm 1.957 / 0.1094 1.493 / 0.0960

      On review (6840111036) this reads as bf16 precision on a two-step tie, not a defect. The fail stands, and a new
      gate on fresh clips is being agreed on the thread before any run.

  • Planted: q_norm and k_norm swapped in every layer fails 11 steps (0.6B) and 2 (1.7B), with exit 1. The audio
    rows one position late move no top token, but they raise the worst fp32 distance to 14.26 / 7.68.

Distance to the package's fp32 run, on 51dc03072

At the final logits, teacher-forced on the package's greedy ids. Each cell is worst column / mean |diff|:

checkpoint dev clip ours package bf16
0.6B 0000 0.777 / 0.0963 1.428 / 0.1023
0.6B 0001 0.795 / 0.0873 1.040 / 0.0989
0.6B 0004 0.807 / 0.0835 0.873 / 0.0911
1.7B 0000 0.757 / 0.0683 0.663 / 0.0734
1.7B 0001 0.611 / 0.0684 0.707 / 0.0733
1.7B 0004 0.805 / 0.0868 0.980 / 0.0819
0.6B cut50, 9 steps 0.667 / 0.0867 0.938 / 0.1226
0.6B cut50, step 3 alone 0.667 / 0.1191 0.536 / 0.0843

On #2285's pass, our mean distance is below the package's bf16 run in 5 of 6 dev cells. The worst column is below it
in 5. On cut50 the whole clip is below it in both; step 3 alone is above it in both.

cut50 step 3 alone: the same clip and tool, with the case's prompt extended by the forced ids of steps 0-2. As a
check, the step 0-2 and step 0-3 slices give the same step-3 mean by difference (0.1191 ours, 0.0843 bf16).
In r4 the 0.6B means were 0.133 / 0.102 / 0.085.

Decode against prompt, two passes against one, on 51dc03072

  • CUDA (tf-qwen3-asr-forced --rows 8, no other flag): the prompt pass's last 9 rows equal the 9 decode steps bit
    for bit. A prompt split in two passes inside the audio rows leaves 0 cache values different and the same last logits.
    That holds on all six dev runs (0.6B and 1.7B, three clips each), because gemm.zig keeps one algorithm per (n, k)
    at every row count.
  • Host (lm_test.zig): the same two equalities, and a planted cache-slot fault fails them.

#2285's embedding bytes

The default call issues the same launches with the same arguments. The new fields are only read when set, and the
plan list is unchanged. Measured on the GB10 with Qwen3-Embedding 0.6B, 4B and 8B (Arminova's revisions,
serve --context 32768), main 000b534a2 against 51dc03072, sha256 of every returned fp32 vector:

  • every vector is byte-equal between main and this head, in all three sizes: the 4 card texts (alone, together, and
    as token ids), seeded inputs of 2048, 8192, 16384 and 30000 ids (alone, 7 inputs in one request, and 2048 with
    30000);
  • every batched row equals its input alone;
  • the card texts equal Arminova's 4257e6f01 hashes, 4 of 4 in each size.

All 8 in one request is refused on both builds by the 32768-token admission.

Kernels

  • New kernels:
    • tf_vit_conv_cols: 3x3 stride-2 conv taps, channels last;
    • tf_vit_conv_flatten: torch's [c, f] rows;
    • a vision_kernel<64, 8> instance for windowed attention at head dim 64.
  • qwen3-asr-contract: all 3 kernels pass gates 1-4, and both planted controls (one per copy kernel) are caught.
    These kernels compile to the same SASS on this head (below).
  • Usage records for nvcc 13.0 on sm_80/86/89/120/121. Under nvcc 13.2, vision_kernel<64, 8> takes 128 registers
    on sm_120/121 instead of 120 and has its own record; the two copies compile the same.

Checks

  • Rebased on main 000b534a2. One conflict: contract_controls_set.cu's include line; it keeps both main's
    contract_dense_controls.cuh and ours.

  • vit.cu and prefill_attention.cu at nvcc 13.0.88, main against this head, sm_80/86/89/120/121 (after main's
    two-query-heads change):

    • vit.cu: 55 of 55 records identical in res-usage and SASS;
    • prefill_attention.cu: 70 of 70;
    • only the three new functions are added.
  • zig build test-gradient-serving, each head merged locally with main 6c5cef894: red on 51dc03072
    (prefill_attention.cu); on 89d85b860 the three re-pinned rows pass, and it stops only at
    zig/src/cuda/experts.zig, which main has since re-pinned (037e9d9018). With that row re-pinned locally too,
    it's green.

  • On 51dc03072: every check exits 0: git diff --check, zig fmt --check, cuda_common_check, lean (0 new), pr_check, zig build test, test-cuda-host, test-qwen3-asr-host, tf-qwen3-asr and tf-qwen3-asr-forced, test-qwen3-cuda-host, test-qwen3-checks-host, qwen3-cuda-gpu-build, the CI set (test-kernel-contract, test-golden, test-server-security, test-pull-security, fuzz-cortheon), compile-cuda-tests for aarch64 and x86_64, and check-release-paths.

  • x86 host set on 51dc03072 (test, test-golden, test-kernel-contract, test-server-security,
    test-pull-security, fuzz-cortheon, check-release-paths, check-native-cuda-host, compile-cuda-tests,
    pr_check, zig fmt): all exit 0.

  • Seeded sampling and concurrency don't apply yet: nothing here is served (no lane until Nemotron 3.5 Transcribe: audio input, the 65,536-token window, an hour of audio, MTP drafts #1632), and the tools are
    greedy on one stream. Resume is the two-pass split above (caches and logits bit-equal to one pass).

CHANGELOG (Unreleased, 2fe5b72d9): qwen3-asr: Qwen3-ASR 0.6B and 1.7B on CUDA (tower and greedy decode), the language model through the Qwen3 CUDA pass (#2239).

Next

The served path (a qwen3_asr CUDA lane backend) waits for #1632. WER/CER, throughput and the 1.7B's eval follow.

@jschmied

jschmied commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review. #2225 has landed, so I rebased onto main 9200a3064 and dropped the two cherry-picked tokenizer commits. Five own commits, head 66d573976. On this head:

  • prompt ids equal qwen-asr's on the dev clips;
  • 38 of the 40 gate clips are byte-identical, with the same two near-ties as before;
  • the full verify is green (lean 0 new, zig build test and the CI set RC 0).

A Metal binding of the same schedules passes on an M3 Pro (3 dev clips byte-identical). It follows as its own PR, stacked on this one.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied. 38 of 40 clips byte-identical, the other two at a bf16 tie and a 0.125 near-tie, with stage-by-stage cosines beside them, is a strong first gate. Two things before it can go further. First, this adds the family and uses the shared-schedule layout from #2118, and both still need a decision on our side; we'll answer on #2105 and #2118, so please keep this as a draft until then. Second, a small one for whenever it moves: vit.cu and prefill_attention.cu are shared by other towers, so please show the existing kernels' records are unchanged against main at nvcc 13.0. Also, could mel.zig's header describe the computation (a centered, reflect-padded STFT and slaney mel filters) instead of naming transformers' functions? On the windowed reference, we'll answer with the family's gate.

@jschmied
jschmied marked this pull request as draft November 5, 2026 12:55
@jschmied

jschmied commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Back to draft until #2105 and #2118 are answered.

Shared kernels at nvcc 13.0. I compiled vit.cu and prefill_attention.cu from main a6e25faea and from this head with the catalog's flags (glue for vit, -O3 --fmad=false for prefill attention, the torch defines) for sm_80, 86, 89, 120 and 121, then compared cuobjdump -res-usage and -sass per arch and function:

unit functions on main resource records identical SASS identical added here
vit.cu 11 x 5 archs 55 / 55 55 / 55 tf_vit_conv_cols, tf_vit_conv_flatten
prefill_attention.cu 9 x 5 archs 45 / 45 45 / 45 vision_kernel<64, 8>

So every kernel main already has compiles to the same registers, shared and constant bytes and the same instructions; the PR only adds the three functions, and their registry rows are in the PR. nvcc cuda_13.0.r13.0/compiler.36424714_0.

mel.zig. 5df1d8e7e rewrites the header and the four comments that named the reference's functions. They now describe the computation: periodic Hann frames of a centered, reflect-padded STFT, slaney triangles over 0 to 8 kHz, log10 floored at the clip's peak - 8, then (l + 4) / 4.

Also on this push: 8548b14de is zig fmt on four files, which turns the fmt job green. Verify on 5df1d8e7e: lean 0 new, zig build test, the no-GPU CI set and zig fmt --check pass.

@shuuul

shuuul commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Thanks — the host/CUDA split is easy to follow, and tapping every stage in the case generator is useful. I went through the host and CUDA paths and found two issues I think should be fixed before this is usable as a 0.6B/1.7B gate, plus a few smaller points.

1. A single chunk shorter than 100 frames uses the wrong convolution geometry

zig/src/families/qwen3_asr/tower.zig:150-171 and tower_ref.zig:88-149 always pass chunk_frames (100) as the convolution's time length. The package does not pad a lone short chunk: Qwen3ASRAudioEncoder.forward sets chunk_lengths[tail] = feature_lens % (n_window * 2) and then calls pad_sequence to the longest chunk. With one chunk under 100 frames the longest chunk is that chunk itself, so the convolutions see its real length; with two or more chunks the tail is padded to 100, which is what this PR currently does everywhere.

This is not only the shape of the conv dumps: the right-edge conv2/conv3 outputs read the extra padded rows, so the kept rows can differ. I reproduced it with a 50-frame (0.5 s) clip:

  • embed cos 0.993416, audio_rows cos 0.973611 against the package case;
  • the case comparison then stops with error: StageSize at the conv stages;
  • a 499-frame case and a 105-frame case with a 5-frame tail both pass, so the problem is specifically a single short chunk.

Suggested fix: when plan.chunks == 1 and plan.frames < chunk_frames, pass plan.frames as cf through the three convolutions (or zero the padded rows after conv1/conv2; the former also makes the stage dumps match the package's shapes). Please add a 50/99-frame case to the host checks.

2. tf-qwen3-asr-check cannot open the released 1.7B checkpoint

zig/qwen3_asr_check.zig:117 hardcodes MODEL_DIR/model.safetensors. The released Qwen/Qwen3-ASR-1.7B is sharded (model-00001-of-00002.safetensors, model-00002-of-00002.safetensors, plus model.safetensors.index.json), so the host checker fails with error: FileNotFound. The CUDA Model.load already uses core.Checkpoint.openModel; the host checker should use the same abstraction (or combine the shards) so the 1.7B gate is actually runnable.

If the host checker is intentionally 0.6B-only, please state that in the tool usage and PR description; otherwise this contradicts the stated 0.6B/1.7B scope.

3. Smaller points

  • zig/qwen3_asr_check.zig:145-146 returns 0 after printing the summary, and zig/tests/qwen3_asr_cuda.zig:196-198 returns 0 even when the transcript is different. My 499-frame case printed audio_rows cos 0.999099 and worst stage cosine 0.998624777, and still exited 0. If these are gates, add a threshold/transcript assertion and a non-zero exit; if they are report-only, please label them that way.
  • config.zig:111 admits audio.out independently from text.hidden; lm.zig:92 copies t.hidden-wide rows from the tower output, so a malformed config can read past the tower rows. Add audio.out == text.hidden to admission.
  • config.zig:118 accepts window_infer < chunk_frames; chunks.zig:36,45 then makes window_rows == 0 and divides by zero. Require window_infer >= chunk_frames. Likewise, Options.group == 0 makes front's first += w.group loop forever.
  • prompt.zig:25-31 claims to implement parse_asr_output, but it does not strip special tokens, fold repetitions, normalize the language name or handle a forced language. It is only used by its own test; either delete it from this slice or align it with the package.
  • qwen3_asr_case.py --ids-only produces no stage files, but the host checker has no switch for that mode and fails on the missing mel stage. The CUDA tool has --ids-only; the host checker should match it.
  • The CUDA path dumps conv1–conv3 but not conv_out, so the conv_out case tensor is never compared. Add it if the stage gate is meant to be complete.

Windowed vs global attention

Windowed attention is defensible because the vLLM backend uses windows, but the package's stock SDPA path is global and the case generator's default windowed mask comes from a hook. Please record this as an explicit decision and keep the --global-attention case checkable; at the moment tower_ref and the host checker only implement the windowed plan, so that path cannot be validated from the same case.

I ran zig build test-qwen3-asr-host, zig build tf-qwen3-asr-check and zig build tf-qwen3-asr successfully. I could not execute the CUDA kernels in my environment (the build reported BuiltWithoutKernels), so the CUDA execution numbers still need a machine run; the short-chunk issue is in the shared tower.zig schedule and the host reference.

@jschmied

jschmied commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @shuuul, all fixed on 85f93a15a, one commit a point:

  1. A lone short chunk (6130afa16). The plan now carries the longest chunk's frames, and both the shared tower schedule and tower_ref run the three convs at that length. So a single chunk under 100 frames is convolved at its own length, and with two or more chunks each one is padded to 100. I made cases with qwen3_asr_case.py from a 0.5 s and a 0.99 s cut of a dev clip. On the host check, audio_rows cosine is 0.99984 / 0.99992 on the 0.6B and 0.99993 / 0.99992 on the 1.7B, and the conv stages are at the package's shapes. The parent commit reproduces your result on the 50-frame case: audio_rows 0.889, then StageSize. The schedule test now also runs 50 and 99 frames against the float64 reference and asserts the conv1 length.
  2. Sharded 1.7B (c09b3dd66). tf-qwen3-asr-check opens the folder through core.Checkpoint.openModel. On the 1.7B it reports WEIGHTS ok for all 708 tensors.
  3. Smaller points:
    • Gates (4e3f90e83). Both tools exit 1 on failure. tf-qwen3-asr-check fails when the worst stage cosine is under --min-cos (default 0.995). tf-qwen3-asr with a case fails on a differing prompt or transcript, or a stage under --min-cos (default 0.99). SUMMARY prints PASS or FAIL.
    • Admission (6f9412133). Admission now requires audio.out == text.hidden and n_window_infer >= chunk_frames, and tower.Weights.load refuses group == 0. The modulo check already refused a window under one chunk, since the integer fields are at least 1. It is now explicit and tested, and a test admits the 1.7B's released geometry.
    • prompt.split (3a41880fb). Deleted. Nothing outside its own test used it.
    • --ids-only on the host check (f44218f08). With it, the tool checks the weights, the prompt ids and the reference reply's decode, and compares no stages.
    • conv_out (375eb01b7). When a sink is attached, the schedule runs conv_out once more without the positions and dumps it. tf-qwen3-asr compares it. The timed pass is unchanged.
    • Windowed vs global (59bb92507). Windowed attention is the decision, matching the vLLM backend and flash-attention-2, and chunks.zig records it. The case manifest names its attention, and tower_ref and tf-qwen3-asr-check run either kind. tf-qwen3-asr runs only windows, so it refuses a global case. On dev clip 0004 (4 windows) with the 0.6B, the --global-attention case passes at audio_rows 0.99987, and the same case checked with windows fails at 0.919.

Host checks on this head, each step with its own exit code, all 0: git diff --check, zig fmt --check, lean (0 new), zig build test, test-cuda-host, test-qwen3-asr-host, the no-GPU CI set (test-kernel-contract test-golden test-server-security test-pull-security fuzz-cortheon), compile-cuda-tests for aarch64 and x86_64, and check-release-paths. 85f93a15a only makes comments one line for the lean check.

The GPU on our box is down until a reboot. The CUDA re-run follows after it: the 0.6B and 1.7B 40-clip gates plus the 50- and 99-frame cases with stages. I'll post the results here.

@jschmied

jschmied commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

The CUDA gates on 85f93a15a are in, on a GB10: the 40 public clips plus the 50- and 99-frame cases and the others in gate-review (45 in all).

checkpoint byte-identical differs
0.6B 42 of 45 3
1.7B 44 of 45 1

All four differences are near ties against the package, with the top two logits at most 0.25 apart on either side:

clip first difference ours (margin) package (margin)
1.7B 1089-134686-0000 token 21 323 (0.25) 11 (0.00)
0.6B cmn-10104380129516909856 token 27 3837 (0.25) 1773 (0.00)
0.6B cmn-15324743446256875074 token 10 468 (0.125) 9197 (0.25)
0.6B cut50 (the new 50-frame case) token 3 5632 (0.25) 18284 (0.125)

The two cmn clips differ the same way on the gate before this review. cut50 is new, and it's a 0.5 s clip. Its audio rows match on the host at cosine 0.99984, so I read it as the same kind of tie, not as the geometry bug.

Since the tools now exit 1 on a differing transcript, these clips fail by exit code, which is what a gate should do. If you'd rather have a margin rule for exact and near ties, say which, and I'll add it.

@ashhart

ashhart commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied for taking every point from @shuuul's review, and for the GB10 rerun with each difference listed with its margins. Thanks too, @shuuul, for the careful read. Nothing is needed from you while this is a draft. The family and layout questions on #2105 and #2118 are still with the maintainer, and we'll answer your margin-rule question with that decision. As we said on #2105, the audio route (#1632) comes first.

  • Synthena (Ash's Agent)

@ashhart

ashhart commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied for the GB10 rerun with every margin listed, and @shuuul for the careful review. The family is a yes (#2105), and this layout is fine for a new family (#2118).

On your margin question, here is the rule. It's the one we used on #2648.

  • Teacher-forced. Run each clip on the reference's greedy ids to the end, so every step is compared on the same history. Byte-identical clips already pass, so only the four that split need their remaining steps checked.
  • A step where our top token differs passes only if the same two tokens swap places (our runner-up is the reference's pick and theirs is ours), and both top-1/top-2 margins are at most 0.25 logit.
  • Every such step is listed with clip, step, both tokens and both margins. Anything else fails and the tool exits 1. A planted change that moves a real token should fail it.

Before it leaves draft:

  1. Rebase on main. It conflicts in four shared files, and contract_controls.cu is already at 600 lines there, so the new include needs another home. Please repeat the vit.cu and prefill_attention.cu SASS comparison on the rebased head.
  2. One Qwen3 forward on CUDA. qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285 lands first, and the language model here runs through its CUDA pass, adding the decode step and the audio-row splice there. Its projections pin one cuBLASLt algorithm per shape, so a row's bits don't depend on how many rows share a pass, and the served path needs that for resumed and concurrent requests. Where the backend-neutral schedule lives for Metal and XPU is yours to propose, as long as CUDA ends with one Qwen3 layer path and qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285's embedding bytes don't move. @Arminova, your review there would be welcome.
  3. The decode step's logits equal the prompt pass's for the same rows, and a prompt run in two passes gives the same cache and logits as one.
  4. At the final logits, our distance to an fp32 run of the package, beside the package's own bf16 distance, worst column and mean, on the dev clips.
  5. A line in the description saying the family was written from TensorFold's code and the publisher's model description, not from another engine's code.

The windowed reference is right, with the global comparison listed as you have it. Since this doesn't touch the server, it can land before #1632. The served path waits for #1632.


  • Synthena (Ash's Agent)

@jschmied

Copy link
Copy Markdown
Contributor Author

@ashhart thanks. Everything below is on b435cccc3, rebased on main 33ada77c3. It's still a draft.

1. Rebase.

  • The four shared files resolved. The controls include moved to contract_controls_set.cu:5, so contract_controls.cu stays at main's 600 lines. The records join in registry_joins.zig:20 and :34, beside MOSS-Image's.
  • SASS at nvcc 13.0 (cuda_13.0.r13.0/compiler.36424714_0). I compiled vit.cu and prefill_attention.cu from main and from this head with the catalog's flags for sm_80, 86, 89, 120 and 121, then compared cuobjdump -res-usage and -sass per arch and function:
unit functions on main x archs res-usage identical SASS identical added here
vit.cu 11 x 5 55 / 55 55 / 55 tf_vit_conv_cols, tf_vit_conv_flatten
prefill_attention.cu 12 x 5 60 / 60 60 / 60 vision_kernel<64, 8>

Tie rule. tf-qwen3-asr-forced (zig/tests/qwen3_asr_forced.zig) runs each clip on the case's greedy ids to the end. Each step goes through forced.judge (zig/src/families/qwen3_asr/forced.zig:13), and every differing step is listed. Anything else fails with exit 1. When topk lists an exact reference tie (margin 0) the other way round, the greedy id counts as the reference's pick. I ran 45 clips per checkpoint: the 40 gate clips, the 50- and 99-frame cuts and the 3 dev clips.

checkpoint clip step ours (margin) reference (margin) verdict
0.6B cut50 3 5632 (0.25) 18284 (0.125) swap, pass
0.6B cmn-10104380129516909856 27 3837 (0.25) 1773 (0.000) swap, pass
1.7B 1089-134686-0000 21 323 (0.25) 11 (0.000) swap, pass
0.6B cmn-15324743446256875074 10 468 (0.125), runner-up 9197 9197 (0.25), runner-up 36 fail

So the 0.6B passes 44 of 45 clips and the 1.7B passes 45 of 45. Every other step matches. The failing step isn't a swap. In the package's bf16 run our 468 is third: 9197 25.75, 36 25.5, 468 25.375. The package's fp32 run of the same step puts 468 first, at 25.7277 against 9197's 25.7162. Under the rule as written it fails. I've left the rule alone and listed the step.

Planted changes on dev clip 0000:

  • q_norm and k_norm swapped in every layer (--plant norms): the 0.6B fails 11 steps and the 1.7B 2, with exit 1.
  • The audio rows one position late (--plant splice): no top token moves, so the tie rule passes it. The fp32 distance in item 4 catches it, with a worst column of 14.45 on the 0.6B and 8.06 on the 1.7B against about 1 unplanted.

2. One Qwen3 forward on CUDA through #2285. Here's what I propose. I'll build it once #2285 lands, and nothing here depends on it yet.

3. Decode against prompt, two passes against one.

  • forwardHeads (lm.zig:101) puts the head over a pass's last rows. The splice is cut to the pass (lm.zig:56), so a prompt can be split inside the audio rows.
  • On the host, a binding computes every op row by row (lm_test.zig:196, :212). Each decode step's logits equal the prompt pass's rows, and a prompt cut at 3, 5, 7 or 11 leaves the same caches and logits as one pass. A planted fault that writes k and v at the pass's row instead of its position fails the first test.
  • On CUDA, tf-qwen3-asr-forced --rows 8 runs the same two checks on each dev clip. With today's vit.Gemms, which chooses its cuBLASLt algorithm per row count, they aren't bit-equal: 0 or 1 of 9 rows equal (worst |diff| 0.375 to 1.625), and the two-pass cache differs on 0000 and 0001. With --row-gemms, every projection runs one row at a time on one algorithm. Then all 9 rows are bit-equal and no cache value differs, on all six runs. So the rest of the pass is already independent of the row count, and the gap is the per-row-count algorithm that qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285 pins.

4. Distance to the package's fp32 run (teacher-forced on the package's greedy ids, final logits, worst column / mean |diff|). qwen3_asr_case.py --logits writes the package's bf16 and fp32 logits:

checkpoint dev clip ours package bf16 ours, --row-gemms
0.6B 0000 1.079 / 0.1329 1.428 / 0.1023 0.750 / 0.0899
0.6B 0001 0.970 / 0.1015 1.040 / 0.0989 1.024 / 0.0917
0.6B 0004 1.260 / 0.0851 0.873 / 0.0911 0.768 / 0.0849
1.7B 0000 0.968 / 0.0737 0.663 / 0.0734 0.757 / 0.0677
1.7B 0001 0.845 / 0.0687 0.707 / 0.0733 0.692 / 0.0827
1.7B 0004 0.667 / 0.0706 0.980 / 0.0819 0.942 / 0.0797

Today's path isn't uniformly closer to fp32 than the package's bf16. On 0.6B 0000 its mean is 0.133 against 0.102, and that's the clip where the multi-row algorithms differ most. One algorithm a shape brings that mean to 0.090. I'll repeat this table through #2285's pass.

5. Provenance. The description now has the line, worded to match what I actually read: TensorFold's code, the checkpoint's config files, and the publisher's own reference package qwen-asr 0.0.6. From that package I read its transformers-backend modeling and processor, its inference wrapper, and a search of its vLLM-backend file for the attention windows. I also checked the log-mel front end against transformers' WhisperFeatureExtractor source, which the package calls. No code was copied from any of them.

Verify on b435cccc3: git diff --check, zig fmt --check, lean (0 new), pr_check, zig build test, test-cuda-host, test-qwen3-asr-host, the CI set, compile-cuda-tests for aarch64 and x86_64, check-release-paths, and on the x86 box the host CI set, check-native-cuda-host and compile-cuda-tests, pr_check and zig fmt --check: all exit 0. The description is updated, provenance line included.

@ashhart

ashhart commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied. The rebase, the SASS table, the tie-rule run with every split listed, the fp32 distances and the provenance line answer what we asked. The #2285 plan (one Qwen3 layer path on CUDA, the schedule in families/qwen3/schedule.zig, start, splice and head read only when set, plus an embedding hash on main against the head) is the shape we asked for.

Two decisions from our side:

  • 0.6B cmn-15324743446256875074 step 10: we count it as a listed exception, not a fail. Our pick (468) is the package's own fp32 pick, and the bf16 reference has it third; please keep it listed with that fp32 evidence in the description.
  • Provenance: reading the model's own release package for behaviour, with nothing copied and the notice you wrote, is fine. Before it lands we run our own copy check against those sources; nothing is needed from you for that.

One small thing for now: the exact-tie swap in tests/qwen3_asr_forced.zig:154 decides two of the listed steps, but nothing tests it. Could it move into forced.zig next to judge, with a unit test that fails without it?

Once it runs through #2285's pass, please post the tie-rule run, the fp32 distance table, and --rows without --row-gemms, each on that head.

  • Synthena (Ash's Agent)

@jschmied

Copy link
Copy Markdown
Contributor Author

@ashhart thanks. On 65b33d6f9, pushed on top of b435cccc3 with no rebase:

  • Exact-tie order. The tool's inline swap is now forced.reference (zig/src/families/qwen3_asr/forced.zig:20), next to judge. tests/qwen3_asr_forced.zig:152 calls it.
    • Its test (forced.zig:71) covers both orders of an exact tie, a non-zero margin with the greedy id second (refused), and a greedy id outside the pair (refused). It also covers the 1.7B 1089-134686-0000 step 21 case, which is a swap.
    • With the swap planted out, the test goes red (27/28 tests passed (1 failed)). With it back, test-qwen3-asr-host is green at 28/28.
  • Description. 0.6B cmn-15324743446256875074 step 10 is now a listed exception, with the fp32 evidence: our 468 is the package's fp32 pick (25.7277 against 9197's 25.7162), and the bf16 reference has it third. The tool still lists it and exits 1 on it.
  • After qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285: on that head I'll post the tie-rule run, the fp32 distance table, and --rows without --row-gemms.

Verify on 65b33d6f9: git diff --check, zig fmt --check, lean (0 new), pr_check, zig build test, test-cuda-host, test-qwen3-asr-host, the CI set, compile-cuda-tests for aarch64 and x86_64, check-release-paths, and the x86 host set. All exit 0.

@Arminova

Copy link
Copy Markdown
Contributor

@jschmied yes, that shape works for us: one Qwen3 layer path on CUDA, the start, splice and head read only when set, and the embedding hashes on main against the new head.

Three things from our side:

For the embedding hashes: we're rerunning #2285's receipts for the 0.6B, 4B and 8B on its rebased head now, and we'll post the per-vector hashes with them, so your comparison has a fixed baseline.

@ashhart

ashhart commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied. The exact-tie order now lives in forced.reference (zig/src/families/qwen3_asr/forced.zig:20-24), with its test at :71 covering both orders and the 1.7B step 21 swap, and the description lists the 0.6B step 10 exception with its fp32 evidence. That's everything we asked for now. Thanks too, @Arminova, for confirming the shape on the #2285 side.

Nothing is needed until #2285 lands. When you rebase onto it, two small things from today's main: contract_controls_set.cu now conflicts on one include line (main added contract_dense_controls.cuh beside yours), and main changed prefill_attention.cu for two query heads a block, so please redo that unit's SASS comparison on the rebased head, along with the tie-rule run, the fp32 table and --rows without --row-gemms.

  • Synthena (Ash's Agent)

…hart#2105)

Qwen3-ASR's 0.6B and 1.7B admitted from config.json: the audio tower's geometry (chunks of 100 frames, windows of
n_window_infer frames) and the Qwen3 language model's, its interleaved M-RoPE refused unless its sections span the
head (audio positions are equal on all three axes, so it is plain RoPE). Every stored tensor checked by name, shape
and dtype, none left over; the stored lm_head must be bit-equal to the tied embedding.

WhisperFeatureExtractor's log-mel rows in float64, and the tower in float64 stage by stage, either exact or at
torch's bf16 rounding points. The checkpoint ships no tokenizer.json; the shared tokenizer reads its slow export
(ashhart#2225).

tf-qwen3-asr-check compares weights, prompt ids, the reply's decode, mel rows and every tower stage with a case from
tools/zig/qwen3_asr_case.py (the qwen-asr 0.0.6 package's transformers backend, hooked stage by stage).
…ing (ashhart#2105)

tower.zig and lm.zig load weights by name through the binding's put and
run the schedule through r.conv, r.linear, r.attention and the rest; they
import no backend. schedule_test.zig runs both on a host binding: the
tower matches the float64 reference at torch's points (global attention
fails it), and the LM's ops stay inside their buffers.
…case's bf16 and fp32 logits (ashhart#2105)

Each clip runs on the reference's greedy ids to the end. A step whose top token differs passes only if the two tokens swap places and both margins are at most 0.25 logit; every such step is listed, anything else fails with exit 1. With logits_fp32 and logits_bf16 in the case (qwen3_asr_case.py --logits), it prints our distance to the package's fp32 run beside the package's own bf16 distance. --rows K checks the decode steps against one prompt pass and a prompt split in two passes against one, on CUDA.
…y round, plants swapped q and k norms, and --row-gemms (ashhart#2105)

topk may list an exact tie (margin 0) in either order; the greedy id is the reference's pick. --plant norms swaps q_norm and k_norm in every layer, a planted change that moves real tokens. --row-gemms runs every projection a row at a time, so --rows can tell cuBLASLt's per-row-count algorithms from the schedule's positions and caches.
…o forced.zig, with its test (ashhart#2105)

forced.reference takes the reference's step from its topk pair and greedy id: an exact tie (margin 0) listed the other way round is the greedy pick's; anything else whose first token is not the greedy id is refused. Its test fails without the swap.
…ith a start position, an audio splice and a tied head over chosen rows (ashhart#2105)

qwen3/cuda/lm.zig's forwardFrom takes an Extra: the input's first position, every layer's own k and v caches,
rows spliced over the embedded ones, the final norm and the tied head over the pass's last rows, and stage taps.
Each is read only when set: forward(.{}) issues the embeddings' launches with the same arguments.
qwen3_asr/cuda/lm.zig (the schedule's CUDA binding) goes; the ASR checkpoint loads through
weights.Model.fromCheckpoint under its thinker.model. scope, and the position and RoPE uploads are lm.tables,
shared with the engine. tf-qwen3-asr-forced drops --row-gemms: every projection keeps one algorithm per (n, k).
… its caches before any launch, with its tests

lm.admit names the refusals (InvalidBatch, ContextFull); its test and Splice.within's run in test-qwen3-cuda-host.
@jschmied

Copy link
Copy Markdown
Contributor Author

@ashhart #2285 is in, so here is the rebase and the run through its pass. Head 51dc03072, on main 000b534a2.

  • Rebase. One conflict: contract_controls_set.cu's include line now has main's contract_dense_controls.cuh
    and ours, both kept.
  • SASS, prefill_attention.cu (after main's two-query-heads change), nvcc 13.0.88, sm_80/86/89/120/121, main
    against this head: 70 of 70 records are identical in res-usage and SASS. vit.cu: 55 of 55. Only our three new
    functions are added.
  • One Qwen3 layer path. The language model now runs qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285's pass through forwardFrom(…, Extra)
    (zig/src/families/qwen3/cuda/lm.zig:343, Extra at :200): a start position, every layer's own caches, the
    audio splice, the tied head over the pass's last rows, and stage taps. Each is read only when set, so forward is
    forwardFrom(.{}) with the same launches and arguments as before.
    • lm.admit (:210) refuses a bad combination before any launch. Its test is at :218; with the cache check
      planted out it goes red (18/19).
    • qwen3_asr/cuda/lm.zig and --row-gemms are gone. The checkpoint loads through weights.Model.fromCheckpoint
      (weights.zig:46), and the position and RoPE uploads are lm.tables (lm.zig:127), which the engine calls too.
    • Part B isn't open yet, so the start is Extra.start for now. I'll move it onto B's attention offset when B
      lands.
  • --rows 8 without --row-gemms. On all six dev runs (0.6B and 1.7B), the prompt pass's last 9 rows equal the
    decode steps bit for bit. The two-pass split leaves 0 cache values different and the same last logits.
  • fp32 distance table: the means are below the package's bf16 run in 5 of 6 cells (0.6B 0000 is 0.0963 against
    0.1023; it was 0.133 on r4). The full table is in the description.
  • Tie rule: 1.7B passes 45 of 45 (four swaps listed). The 0.6B passes 43 of 45, with step 10 as the listed
    exception. One new fail needs your call: 0.6B cut50 step 3.
    • Ours is 5632 with margin 0.625. The reference case has 18284 at 0.125.
    • The package's own teacher-forced logits disagree with that case. Its bf16 run puts 5632 first (22.75 against
      22.625), and its fp32 run puts 18284 first (22.7922 against 22.5113).
    • On r4's bf16-rounded norms this step was a swap at 0.25. qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285's fp32 norm weights move it to 0.625.
    • Should it be a second listed exception, or should the ASR keep bf16-rounded norms through a second norm point in
      the pass? We'd rather not add that point. It stays draft until you decide.
  • Embedding bytes: not run yet. The Qwen3-Embedding 0.6B/4B/8B checkpoints aren't on this box, and I'd need
    @Arminova's id lists for inputs 4-7. The launches are the same by construction. I'll post the 36 hashes against
    4257e6f01 once the checkpoints are here.

Verify on 51dc03072: Every check exits 0: git diff --check, zig fmt --check, cuda_common_check, lean (0 new), pr_check, zig build test, test-cuda-host, test-qwen3-asr-host, tf-qwen3-asr and tf-qwen3-asr-forced, test-qwen3-cuda-host, test-qwen3-checks-host, qwen3-cuda-gpu-build, the CI set (test-kernel-contract, test-golden, test-server-security, test-pull-security, fuzz-cortheon), compile-cuda-tests for aarch64 and x86_64, and check-release-paths. The x86 host set exits 0 too.
Receipts: the run log and per-clip logs for every row above, on this head.

@jschmied

Copy link
Copy Markdown
Contributor Author

@ashhart @Arminova here are the embedding bytes I owed in the r5 reply. #2285's vectors don't move on this head.

Main 000b534a2 against head 51dc03072, both built with zig build native and the same flags. One GB10, serve --tp 1 --context 32768, page cache dropped before each server start, one model resident at a time. Checkpoints are at Arminova's revisions: 0.6B 97b0c614, 4B 5cf2132a, 8B 1d8ad4ca. Each cell is the sha256 of every returned vector's raw fp32 bytes (base64-decoded from the response), main against head.

input 0.6B 4B 8B
card texts 0-3, each alone 4/4 equal 4/4 equal 4/4 equal
the 4 texts in one request equal equal equal
the texts as token ids (27, 23, 8, 31) equal, and equal to the text equal, and equal to the text equal, and equal to the text
ids 2048 / 8192 / 16384 / 30000, each alone 4/4 equal 4/4 equal 4/4 equal
7 in one request (texts + 2048, 8192, 16384) equal, each row = alone equal, each row = alone equal, each row = alone
2048 + 30000 in one request equal, each row = alone equal, each row = alone equal, each row = alone
texts against Arminova's 4257e6f01 table 4/4 4/4 4/4
  • Long inputs. These are our own seeded ids: random.Random(2239 * 100000 + n).randrange(0, 151643), n of them. sha256 of the little-endian u32 id lists: 2048 bb2ec7d6…, 8192 d6e31106…, 16384 26c945c6…, 30000 f4c02825…. They aren't Arminova's lists, so only main against head applies to them.
  • All 8 in one request (56.6k tokens) is refused on both builds with the same message: "the embedding batch exceeds the admitted 32768 tokens". So the batch check uses the two requests above, which fit, and together they cover every input.
  • Binaries: main 26736cfc, head a7c8acf3 (md5).

@Arminova

Copy link
Copy Markdown
Contributor

@jschmied here are the token ids behind the per-vector hashes in #2285 (#2285 (comment)): https://gist.github.com/Arminova/416fab254972967b7e4307e5fdea25ed

  • Two files: one for the 0.6B, one for the 4B and 8B (their lists are identical; the 0.6B's inputs 5-7 differ, from its own tokenizer run).
  • Each entry has the ids, the input's sha256 (of the ids written as space-separated decimals, the input column of that comment) and our vector sha256 for each size.
  • Ids 0-3 are the model card's two queries and two documents. Ids 4-7 are cuts of a long English text (TensorFold's docs) at 2,048, 8,192, 16,384 and 30,000 tokens, each ending in <|endoftext|>.
  • Each input was sent alone: POST /v1/embeddings with {"input": [ids], "encoding_format": "base64"}. The vector hash is of the returned raw little-endian fp32 bytes.
  • Part B is open as qwen3 cuda: Qwen3 reranker checkpoints (BF16) on the native CUDA engine (#2261, 2 of 2) #2995: its trunk gives attention the offset your start can move onto.

@jschmied

Copy link
Copy Markdown
Contributor Author

Thanks @Arminova for the id lists. All 8 input sha256 matched in each file before any run. On head 51dc03072 (GB10, serve --tp 1 --context 32768, each input alone, base64 fp32), every vector equals your 4257e6f01 table:

  • 0.6B: 8/8 equal
  • 4B: 8/8 equal
  • 8B: 8/8 equal

Part B is #2995. When #2995 lands, I'll move Extra.start onto its trunk offset.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied, and thanks @Arminova for the id lists. Byte-equal vectors on all three sizes, from a second GB10 build, is a strong cross-check.

#2285 is on main as of last night, so this can rebase whenever suits you. #2995 (part B) is in its first review now, and we'll post on it shortly. Moving Extra.start onto its trunk offset after #2995 lands sounds right. Until then, nothing here waits on you.

  • Synthena (Ash's Agent)

@jschmied

Copy link
Copy Markdown
Contributor Author

@ashhart thanks. The rebase is already done: 51dc03072 sits on main 000b534a2, with #2285 in, and it merges cleanly (r5, 6815619096). The one thing still open is in that reply: 0.6B cut50 step 3 after the switch to #2285's fp32 norm weights. Should it be a second listed exception, or should the ASR keep bf16-rounded norms through a second norm point? It stays draft until you decide.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied, and sorry we missed this question in our last reply. Here is the call on 0.6B cut50 step 3: a listed exception, not a second norm point.

Why: one Qwen3 layer path on CUDA with #2285's fp32 norm weights and RoPE table is worth more than matching one bf16 step. On #2285's numerics your distance to the package's fp32 run went down (0.6B 0000 mean from 0.133 to 0.0963), and it is now below the package's own bf16 run in 5 of 6 cells. A second rounding point would pull every other step back toward bf16 to settle one near-tie on the 50-frame cut.

The rule for listing a step, fixed now so it covers both: the package's own bf16 and fp32 runs put the two tokens in opposite orders, and either our pick is the fp32 pick or our distance to the fp32 run on that clip is no worse than the package's bf16 distance. Step 10 meets it (bf16 has 9197 first, fp32 has 468, which is ours). cut50 step 3 meets the first part (bf16 has 5632 first, fp32 has 18284); the second part needs the numbers below.

Two things before it leaves draft:

  1. Please add 0.6B cut50 to the fp32 distance table, ours beside the package's bf16, worst column and mean, for the whole clip and for step 3 alone. If ours is no worse there, list step 3 in the description as the second exception with that evidence, as step 10 is. If it is clearly worse, post the numbers and we'll look at it with you first.
  2. zig build test-gradient-serving pins the bytes of the shared serving kernels and their wrappers (zig/tests/fixtures/gradient-serving.tsv). It runs in the nightly set, not in PR CI, so CI stays green, but it fails on this head: the PR changes three pinned files, zig/kernels/cuda/prefill_attention.cu, zig/kernels/cuda/vit.cu and zig/src/cuda/vit.zig. Your SASS comparison already shows the changes are additive. Please give those three rows their new sha256, with a short note naming the change, as the prefill_attention.cu row already has.

On #2995: this doesn't need to wait for it. Both PRs change zig/src/families/qwen3/cuda/ (lm.zig, engine.zig, weights.zig) and qwen3_cuda.zig, so whichever lands second takes the other's changes there. If this one lands first, moving Extra.start onto #2995's trunk offset can be a small follow-up.

  • Synthena (Ash's Agent)

…s audio tower (ashhart#2105)

The three pinned serving sources gain only new code: a vision_kernel<64, 8> instance, tf_vit_conv_cols and
tf_vit_conv_flatten, and their wrappers. The existing kernels' SASS is unchanged on sm_80/86/89/120/121.
@jschmied

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Head 89d85b860: 51dc03072 plus the re-pin.

1. 0.6B cut50 against the package's fp32 run (worst column / mean |diff|, teacher-forced, same tool as the table):

cut50 ours package bf16
9 steps 0.667 / 0.0867 0.938 / 0.1226
step 3 alone 0.667 / 0.1191 0.536 / 0.0843

Step 3 alone is the clip with the case's prompt extended by the forced ids of steps 0-2. The step 0-2 and 0-3 slices give the same step-3 means by difference.

By your rule, step 3 doesn't qualify. The whole clip is closer to fp32 than bf16 is, but step 3 alone is clearly worse: mean +41%, and our worst column there is 18284 itself (ours 5632 by 0.625; fp32 has 18284 by 0.281, bf16 has 5632 by 0.125). So I haven't listed it: the description has the rows and keeps it as the open fail, and the PR stays draft. Happy to look at it with you.

2. test-gradient-serving: 89d85b860 re-pins prefill_attention.cu, vit.cu and vit.zig, each with a note naming the additions (vision_kernel<64, 8>; tf_vit_conv_cols and tf_vit_conv_flatten; their wrappers and the head-64 entry). Merged with main 6c5cef894 (locally, not pushed):

The branch alone sits on 000b534a2, whose cublaslt.zig pin main fixed later (486b0be1c), so I compared it merged. I left the experts.zig row to main.

#2995: agreed. Whichever lands second takes the other's changes in qwen3/cuda/ and qwen3_cuda.zig. If this lands first, moving Extra.start onto #2995's trunk offset is a small follow-up.

@ashhart

ashhart commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied for the cut50 rows and the re-pin.

What matches. Thank you for these numbers:

  • On cut50 as a whole, yours is closer to the package's fp32 run than its own bf16 run is, in both columns: 0.667 / 0.0867 against 0.938 / 0.1226.
  • The clip's other 8 steps match the reference.
  • The step 0-2 and 0-3 slices give the same step-3 means by difference, which is a good check on the step-3 row.
  • On qwen3 cuda: Qwen3 embedding checkpoints (BF16) on the native CUDA engine (#2261, 1 of 2) #2285's numerics your mean is below the package's bf16 run in 5 of the 6 dev cells.

0.6B cut50 step 3 still fails the gate as set. You applied the rule as we wrote it, and it stays as written: we fixed it before these numbers came in, and we won't loosen it now that they're here. The first part holds: the package's bf16 run picks 5632 by 0.125 and its fp32 run picks 18284 by 0.281. The second part doesn't. Our pick is 5632 by 0.625, not the fp32 pick, and at step 3 ours is further from the fp32 run than the bf16 run is, in both columns: worst column 0.667 against 0.536, mean 0.1191 against 0.0843 (41% higher). The worst column is 18284 itself. So step 3 stays an open fail, not a listed exception, and the PR stays draft. Leaving it unlisted, with both rows in the description, was the right call.

The look we promised. We said we'd look at it with you if step 3 came out worse, so here is what we'd like to see. Please post the layer-by-layer stage compare at step 3 against the package's fp32 run: the stages tf-qwen3-asr checks against a case (the tower, each language-model layer and the final norm), with the prompt extended by the forced ids of steps 0-2. For each stage, give ours against fp32 and the package's bf16 run against fp32, as worst column / mean |diff| like the table, plus the 18284 and 5632 logits at the head. We're looking for the first layer where ours moves away from fp32 by more than bf16 does, the point where 18284 starts to lose its lead. That tells us whether one stage is out of line or the gap builds up across the layers.

Re-pin. The three re-pinned rows in zig/tests/fixtures/gradient-serving.tsv match the files on 89d85b860, and each note names what was added. The zig/src/cuda/experts.zig row was main's own miss after #2843. Main has re-pinned it (037e9d9018), so leave it out here. That commit also runs the serving pins in PR CI, so the next CI run here checks them.

One small thing you can do now: add the CHANGELOG line from your description under Unreleased, ending with (#2239).

Before it lands we'll run our own copy check against qwen-asr 0.0.6, as we said on 10 Nov, and a short GB10 check that Qwen3-Embedding vectors from main and this head are byte-equal. If this lands before #2995, moving Extra.start onto its trunk offset stays a small follow-up, as agreed.

  • Synthena (Ash's Agent)

@jschmied

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Head 2fe5b72d9: 89d85b860 plus the CHANGELOG line.

Stage compare, 0.6B cut50 step 3. The prompt is extended by the forced ids of steps 0-2 (25 ids). Each stage is
compared against the package's fp32 run, as worst column / mean |diff|. The tower is compared over all 7 rows. The LM
stages are compared at the last row, the one that gives step 3. Our stages come from a dump-only build of 89d85b860
(not in the PR). The package's stages come from one thinker pass in bf16, then fp32, with the vLLM windows.

The last column is a logit lens: each layer's row through the final norm and the 18284 and 5632 head rows, in fp64. It
shows 18284's lead over 5632 for ours / bf16 / fp32.

  • Tower: level with bf16 at every stage. Means are within 0.0004 of bf16's, and audio_rows is 0.001 / 0.0002 for
    both.
  • LM layers 0-20: our mean is at or below bf16's at every layer (6 and 7 are within 0.0002). Our worst column is
    above bf16's at layers 4-15, but the lens tracks fp32 closely there.
  • From layer 21: both columns go above bf16's, and our mean stays above it to the head. 18284's lead falls further
    behind fp32, with small ups and downs:
stage ours vs fp32 bf16 vs fp32 lead: ours / bf16 / fp32
lm_layer17 0.073 / 0.0135 0.089 / 0.0184 -0.053 / +0.024 / +0.130
lm_layer18 0.088 / 0.0184 0.211 / 0.0266 +2.293 / +2.437 / +2.474
lm_layer19 0.152 / 0.0268 0.266 / 0.0343 +2.940 / +3.208 / +3.294
lm_layer20 0.298 / 0.0451 0.327 / 0.0485 +3.459 / +3.627 / +3.863
lm_layer21 0.860 / 0.0750 0.431 / 0.0693 +1.224 / +1.509 / +1.843
lm_layer22 0.655 / 0.0973 0.655 / 0.0859 +2.447 / +2.654 / +2.979
lm_layer23 0.748 / 0.1380 0.982 / 0.1084 +4.229 / +4.562 / +5.006
lm_layer24 0.954 / 0.1681 1.353 / 0.1325 +3.658 / +3.947 / +4.361
lm_layer25 0.993 / 0.2126 1.489 / 0.1653 +3.348 / +3.958 / +4.282
lm_layer26 1.050 / 0.2264 1.173 / 0.1923 -0.188 / +0.322 / +0.629
lm_layer27 1.571 / 0.2507 1.194 / 0.2238 -0.637 / -0.045 / +0.281
lm_norm 1.957 / 0.1094 1.493 / 0.0960

Head logits, 18284 / 5632:

  • ours: 22.125 / 22.750;
  • bf16: 22.500 / 22.625;
  • fp32: 22.7922 / 22.5114.

Measured from fp32, ours is 0.18 behind on the lead at layer 18, 0.62 at layer 21 and 0.91 in the head logits. bf16
is 0.04, 0.33 and 0.41 behind at the same points. The first layer where ours moves away from fp32 by more than bf16
does is lm_layer21. The gap builds up over layers 21-27: no single stage is out of line. Step 3 stays an open fail
and the PR stays draft.

Note: this bf16 pass covers 25 rows, so its head logits are one bf16 step lower than the 30-row logits case (22.625 /
22.75). The pick and the 0.125 margin are the same. The fp32 logits equal the case's.

CHANGELOG: 2fe5b72d9 adds the line under Unreleased, ending with (#2239).

experts.zig: the re-pin commit never had that row; I only re-pinned it locally for the merged test run. So
89d85b860 changes only the three rows, and there's nothing to drop.

Full table (every stage) and the scripts: on request.

@ashhart

ashhart commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied for the stage compare at step 3, which is what we asked for, and for the CHANGELOG line on 2fe5b72d9. You're right about experts.zig: against main the fixture diff has only your three rows, so there's nothing to drop.

Below, "lead" is 18284's logit minus 5632's, read through your lens.

Where 18284 loses its lead.

  • In the rows you posted it first trails in ours at layer 17 (-0.053, against bf16 +0.024 and fp32 +0.130). All three are near zero there, and layer 18 puts all three 2.3 to 2.5 ahead.
  • It loses the lead for good at layer 26 in ours (-0.188) and at layer 27 in the package's bf16 run (-0.045). fp32 keeps it to the head (+0.281).
  • Layer 26 isn't where ours goes wrong. It cuts the lead by about the same amount in all three runs: 3.54 in ours, 3.64 in bf16, 3.65 in fp32. Ours flips there because it arrives 0.93 behind fp32 (3.348 against 4.282), where bf16 arrives 0.32 behind.
  • That deficit builds a little at a time. Ours is 0.18 behind fp32 at layers 17 and 18, 0.40 at 20, 0.62 at 21, 0.78 at 23, 0.93 at 25 and 0.91 at the head. It grows by 0.10 to 0.25 at layers 19, 21, 23, 25 and 27 and shrinks by 0.07 to 0.12 at 22, 24 and 26. The package's bf16 run shows a smaller version of the same thing: 0.04 behind at layer 18, 0.41 at the head.

No stage is out of line. Our mean is level with or below bf16's through the tower and layers 0 to 20. After that it is at most 1.29 times bf16's (layer 25: 0.2126 against 0.1653). The one large worst column is layer 21 (0.860 against 0.431), and it is level again at layer 22 (0.655 each). The final norm and head barely move the lead: ours reads -0.637 through the lens at layer 27 and -0.625 at the head.

Our arithmetic or precision. This step is a near-tie. fp32 picks 18284 by 0.281. At logits near 22.5 one bf16 step is 0.125, so that is just over two steps, and the package's own bf16 run flips it too. A defect in our arithmetic, like a wrong scale or a misplaced row, shows as a stage far out of line with bf16, and on every step through it: your planted splice gave a worst column of 14.45 against about 1. Here no stage is. On the clip as a whole ours is closer to fp32 than bf16 is in both columns (0.667 / 0.0867 against 0.938 / 0.1226), and its mean is closer in 5 of the 6 dev cells. Your earlier runs point the same way: the move onto #2285's pass, with its fp32 norm weights in place of bf16-rounded norms, took this step from a 0.25 margin to 0.625, while it brought the 0.6B 0000 mean down from 0.133 to 0.0963.

So we read it as precision: where bf16 rounding falls, ours and the package's, against an fp32 reference on a two-step tie. We can't point to a defect, and nothing here asks for a code change.

What follows.

  • The gate stays as set. Step 3 failed it on this run, and that result stands. It stays in the description as an open fail, with this compare beside it, and the PR stays draft. We won't re-score cut50 under another rule.
  • The way on is a new gate for a new run, agreed here in writing before that run starts. Our proposal, open to your comments:
    1. Clips neither of us has run, picked by a rule fixed first, for example the next N utterances by ID from the same public sets as the 40 gate clips, with each file's SHA-256 posted before the run. Both checkpoints, on the head of the day, teacher-forced as now, against the package's fp32 run with its bf16 run beside it.
    2. Every step whose top token differs from fp32's is listed with both tokens and both margins (CONTRIBUTING.md:78).
    3. A listed step passes only as a swap of the same two tokens with both margins at most 0.25 logit, as in the 10 Nov tie rule (CONTRIBUTING.md:79).
    4. Over each checkpoint's whole set, our worst column and mean to fp32 are no worse than the bf16 run's (CONTRIBUTING.md:77).
    5. A planted change that moves a real token fails it, as --plant norms does today.
  • If you'd rather propose the clip rule or a different number in 3, post it here before running anything. Once we've agreed the text, the run's verdict follows it either way.

The full table isn't needed for this. The rows you posted are enough to sit beside the fail in the description.

  • Synthena (Ash's Agent)

@jschmied

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Agreed: step 3 stays an open fail, the PR stays draft, and the new gate is fixed in writing before
any run. I accept your five points as written. Below are the clip rule and the details the run needs, for your OK.
Nothing has run.

Clips (point 1): 20 from each of the gate's two public sets, 40 per checkpoint, taken by the gate's own rules one
step further.

  • LibriSpeech test-clean: the gate took the first 10 speakers by number. This takes the next 10 (1320, 1580, 1995,
    2094, 2300, 2830, 2961, 3570, 3575, 3729), each speaker's lowest chapter, utterances 0000 and 0001. The source is
    the same tarball, md5 32fa31d27d2e1cad72775fee3f4849a9.
  • FLEURS cmn_hans_cn test: the gate read test.tsv in order and took the first recording of the first 20
    distinct sentence ids. This takes the first recording of the 21st to 40th, from the same revision 70bb2e84b and
    the same tarball, sha256 09d19ad1…c166.
  • Neither of us has run these 40.
  • Why N = 40: it matches the gate set's size. Each checkpoint's set takes about 1 h of CPU for the package's bf16
    and fp32 cases, and a few minutes on the GB10.

How it runs (points 1-5):

  • Teacher-forced on the package's fp32 greedy ids, with the vLLM windows as now.
  • A step is listed when our top token differs from fp32's (point 2). It passes only as a swap of the same two tokens
    with both margins at most 0.25 (point 3).
  • Point 4 is computed per checkpoint over every step of all 40 clips. The bf16 run sits beside it.
  • Point 5: the same run with --plant norms must fail.
  • The head and the binary md5 are named in the post. The verdict follows these rules either way.
The 40 clips, with SHA-256
set id member sha256
ls 1320-122612-0000 LibriSpeech/test-clean/1320/122612/1320-122612-0000.flac 2f914687b13e1864d267282c3749ab6f0b6a5b7dcb2b772f28b6a3af7854859e
ls 1320-122612-0001 LibriSpeech/test-clean/1320/122612/1320-122612-0001.flac 0a79505430aa3995a69d287ed16f8368a889b652ff6f3f4f25c371e07f7f0a20
ls 1580-141083-0000 LibriSpeech/test-clean/1580/141083/1580-141083-0000.flac 59123e88017e4bf645e39506a878b5734b3d94fe990018de25b712ba881cf7a5
ls 1580-141083-0001 LibriSpeech/test-clean/1580/141083/1580-141083-0001.flac 9914a442f04e4fed4a8144ec63326ec3b5f031ccd73203b1a9c309182a172ba7
ls 1995-1826-0000 LibriSpeech/test-clean/1995/1826/1995-1826-0000.flac 74596627a026feb72e3acba17ee9268f120ee25e9f4afb935bb2f3d3ba779e57
ls 1995-1826-0001 LibriSpeech/test-clean/1995/1826/1995-1826-0001.flac 56d8ebeb4138c7cc33c7f468aec078a3d76694b8759b1e860d876288f475e8ee
ls 2094-142345-0000 LibriSpeech/test-clean/2094/142345/2094-142345-0000.flac 557af1a29773cd4d78e62aa9e1ad55e2e46db5cde88b7a84c4692427df765279
ls 2094-142345-0001 LibriSpeech/test-clean/2094/142345/2094-142345-0001.flac b4fb8a76b5026b1cff5193cba298dc66156f93e228a221a06d26790c34cc049f
ls 2300-131720-0000 LibriSpeech/test-clean/2300/131720/2300-131720-0000.flac 84cb3f03857e7a68d5811c2f07f14725f8583a61d3a280816e30ec44956d8d7e
ls 2300-131720-0001 LibriSpeech/test-clean/2300/131720/2300-131720-0001.flac 421a5788b45cc79e2c7eb4cc91ad5ca98a0644a0dab2978436a20fc4cd1853ce
ls 2830-3979-0000 LibriSpeech/test-clean/2830/3979/2830-3979-0000.flac b21719bb12120d2ec798c65e4b251c3dca7a002d2c5fda59859524b171fa9d5c
ls 2830-3979-0001 LibriSpeech/test-clean/2830/3979/2830-3979-0001.flac 46bd81f7e35e2ae044f73e02e977c76dacf907999ae8a1085521a468f4cd9df9
ls 2961-960-0000 LibriSpeech/test-clean/2961/960/2961-960-0000.flac 370a3a9f5cda245004db3e6178a7a9a50919687fb24dea05e661a0ad2e7c1c5d
ls 2961-960-0001 LibriSpeech/test-clean/2961/960/2961-960-0001.flac 73a2c08a15dc492ae512877618f9bd2ba6dd8b3c1bd5a5e8d59ef63d7ef3d5fb
ls 3570-5694-0000 LibriSpeech/test-clean/3570/5694/3570-5694-0000.flac 55049eed3f6a821b44b5b8d0e2a7348fc01692fc79981ec56b140ac0f051579e
ls 3570-5694-0001 LibriSpeech/test-clean/3570/5694/3570-5694-0001.flac 83d42febda01d467c35c0b90dc7bb5d5d80b18ed95d65748a831afca53f2e3cd
ls 3575-170457-0000 LibriSpeech/test-clean/3575/170457/3575-170457-0000.flac 96ab958dda879bdd4bc9fc61c2e5b0d632c569c256b54bcb1c0ecbeadd31448e
ls 3575-170457-0001 LibriSpeech/test-clean/3575/170457/3575-170457-0001.flac 7c88157fc69275c820d08c8ff75c9ef42ccf8588d3255b4feabb878e95a29494
ls 3729-6852-0000 LibriSpeech/test-clean/3729/6852/3729-6852-0000.flac 3fa96f8e0276d79ec027bb1595215a8bbeb488cefcc1702ff798e91ec8504403
ls 3729-6852-0001 LibriSpeech/test-clean/3729/6852/3729-6852-0001.flac d62cde117a1e577e7fced2186f6a031e8341b1677efdd037f2891c8a38946023
fleurs cmn-6904953527599225264 test/6904953527599225264.wav (sentence id 2007) before the run
fleurs cmn-7869813406685007810 test/7869813406685007810.wav (sentence id 1935) before the run
fleurs cmn-15514325222679879585 test/15514325222679879585.wav (sentence id 1679) before the run
fleurs cmn-16170678538190250419 test/16170678538190250419.wav (sentence id 1860) before the run
fleurs cmn-11297400535212590430 test/11297400535212590430.wav (sentence id 1853) before the run
fleurs cmn-7769305805697259437 test/7769305805697259437.wav (sentence id 2001) before the run
fleurs cmn-15610115668837304787 test/15610115668837304787.wav (sentence id 1794) before the run
fleurs cmn-6119869420349327544 test/6119869420349327544.wav (sentence id 1717) before the run
fleurs cmn-2808841965582657611 test/2808841965582657611.wav (sentence id 1745) before the run
fleurs cmn-8796485419234588472 test/8796485419234588472.wav (sentence id 1815) before the run
fleurs cmn-4311928890660363024 test/4311928890660363024.wav (sentence id 1895) before the run
fleurs cmn-6434743737829208648 test/6434743737829208648.wav (sentence id 1684) before the run
fleurs cmn-3633749473030786535 test/3633749473030786535.wav (sentence id 1867) before the run
fleurs cmn-4527058471786347963 test/4527058471786347963.wav (sentence id 1806) before the run
fleurs cmn-5911371549960676194 test/5911371549960676194.wav (sentence id 1798) before the run
fleurs cmn-8866109507232163465 test/8866109507232163465.wav (sentence id 1735) before the run
fleurs cmn-13792944605160254752 test/13792944605160254752.wav (sentence id 1858) before the run
fleurs cmn-5323502856288307832 test/5323502856288307832.wav (sentence id 1994) before the run
fleurs cmn-8538608467298888133 test/8538608467298888133.wav (sentence id 1872) before the run
fleurs cmn-8386178783748510855 test/8386178783748510855.wav (sentence id 1709) before the run

LibriSpeech hashes are of the published FLAC members. The 20 FLEURS clips aren't on disk: I'll fetch the pinned
tarball, check its sha256 and post their 20 hashes here before anything runs.

The description now has the step-3 rows beside the open fail.

@jschmied

Copy link
Copy Markdown
Contributor Author

@ashhart the 20 FLEURS clip hashes I promised, before anything runs. The tarball is data/cmn_hans_cn/audio/test.tar.gz at google/fleurs revision 70bb2e84b976b7e960aa89f1c648e09c59f894dd. Its sha256 09d19ad18f5d7e91076880807e866cd16abd924c7052b55f71cdae91714fc166 equals the pinned value and the HF LFS oid. The hashes below are of the published test/<id>.wav members, in the order of the clip list in my last comment. Nothing has run; the gate text still waits for your OK.

clip sentence id sha256 of the member
cmn-6904953527599225264 2007 74fb2f851e7baffd56a4d94b40129c6e0d6f727a35cd92bc0d4e7aac6a32126c
cmn-7869813406685007810 1935 feb2f2e7cbc68ebe981970c1a5ed8b6edda7d80d11aedf16183b631bbff6e577
cmn-15514325222679879585 1679 49d3217d97b9aca836c9d69cdffa68830f987d126669c0fe69bb22507b20cd51
cmn-16170678538190250419 1860 564dab51d240c07aa8706cc11ac775f7c177e78da59af00c446be2628ebd7526
cmn-11297400535212590430 1853 f056640a286f1c4945be07c2061443af334f694cca4ecd325bde0d64f13adc90
cmn-7769305805697259437 2001 05a2f5e1c01ae7e32e832fcbd94f6acba23b81f2517a61b9371a777a2e4d66fa
cmn-15610115668837304787 1794 5c49056f3c536f6044ca43a2daebafe07a53832402a2f77635870bace925c10c
cmn-6119869420349327544 1717 897778d7405cf61750091a422e3446bc587eaf611fbb670e856ab4e58bec2dbe
cmn-2808841965582657611 1745 cd940932a351fbe900a0a28850950e68a4be57f783fde4ba5f91d2b87e5a261c
cmn-8796485419234588472 1815 99656ebc0d1b135e64b98d53006a836d2c4b0f26f90279d4c43949d0bb8938b3
cmn-4311928890660363024 1895 06649cceb6f4f752c6e3d668e92296a38e386247382998e0eb1a0605c5bc7c82
cmn-6434743737829208648 1684 950c4e15c7c7135087f63dd10494eab30e6e727527a9a373f2516f04dea91ff6
cmn-3633749473030786535 1867 b1727f2a3cef91597412f57d1d127c600d8d4705d3db98883514d79447a7d977
cmn-4527058471786347963 1806 8da0273f97de9344ef9504e86b38c76334efd086fa3803c2311da8d5beeb5ab5
cmn-5911371549960676194 1798 710e62f9c7fda5e7b229ac4f3c09491c27a38b9a84e708a22bd46ebfd72229e7
cmn-8866109507232163465 1735 113ccd863935e24754b0a966343244e42650be62bd77361b7e48a10a92c96e63
cmn-13792944605160254752 1858 09279482fe917a249ffdfa03b965b2a5e253a87840834a5f7a376a55c43902a7
cmn-5323502856288307832 1994 17a8a46af733169f105f79c2a6d49257739ff54d2c5712cced15f50947f4d4e6
cmn-8538608467298888133 1872 eaad053f54912ab695e28a7b3687561f9dfcd85d674a262ebf0f6f0c06f4f53e
cmn-8386178783748510855 1709 95b4565ecc07407d861ce5ff500542968c04cdc14552b7b2fb085eba0cd7ca09

This branch has not been deployed

No deployments
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.

4 participants