Repository navigation
qwen3_5: serve Clef-Flash's BF16 Qwen3.5 backbone on HIP - #2046
Conversation
|
Thanks @laudney, this is a clean base for the Clef-Flash stack. The bf16 format stays opt-in by the config alone, and the MLX path is untouched on the host. Refusing to serve until the readout lands is the right call too.
|
Embed record lines: gate 1 compare report (tools/clef_backbone_rows.py compare){
"sources": {
"torch": "2.15.0a0+rocm10.1.0a20260822",
"transformers": "5.22.1",
"checkpoint": "Cloudflare/clef-flash@fb4417d31c1d0f9e7e77f1141b40aaede7491e00"
},
"buckets": [
{
"bucket": "245-1024",
"records": [
"rec_00",
"rec_01",
"rec_02",
"rec_03",
"rec_04",
"rec_05",
"rec_06",
"rec_07",
"rec_08",
"rec_09"
],
"rows": 6357,
"native_mean": 0.012417430112239893,
"bf16_mean": 0.012646992549515845,
"passes": true
},
{
"bucket": "1025-4096",
"records": [
"rec_10",
"rec_11",
"rec_12",
"rec_13",
"rec_14",
"rec_15",
"rec_16",
"rec_17",
"rec_18",
"rec_19"
],
"rows": 27234,
"native_mean": 0.009537120340056611,
"bf16_mean": 0.010023162834097656,
"passes": true
},
{
"bucket": "4097-10240",
"records": [
"rec_20",
"rec_21",
"rec_22",
"rec_23",
"rec_24",
"rec_25",
"rec_26",
"rec_27",
"rec_28",
"rec_29"
],
"rows": 68786,
"native_mean": 0.009299759542405747,
"bf16_mean": 0.009638283130318349,
"passes": true
},
{
"bucket": "10241-16384",
"records": [
"rec_30",
"rec_31",
"rec_32",
"rec_33",
"rec_34",
"rec_35",
"rec_36",
"rec_37",
"rec_38",
"rec_39"
],
"rows": 137161,
"native_mean": 0.00901123333391803,
"bf16_mean": 0.009409329436653966,
"passes": true
}
],
"top1": []
} |
|
Thanks @laudney, that answers all four.
Two small things, neither blocking:
We don't have an AMD GPU, so your posted runs are the device evidence for this. |
The seeded sampled run beside the greedy one: R9700, main The body now puts the resume rule in the gate 2 paragraph and lists the workspace fix under "What". |
b347a40 to
6d11725
Compare
6d11725 to
0910d8f
Compare
0910d8f to
212f687
Compare
An unquantized HF checkpoint keeps its projections in BF16, and the HIP engine will multiply them as stored through rocBLAS, as torch's Linear does. This adds the bf16 format module (detected only when a config declares no quantization, rank-2 BF16 weights read as mapped) and its tag in the format, decoder and union types, with a refusing arm in every exhaustive switch so the tree builds. The shared read order is unchanged, so no other family sees the new format.
Clef-Flash ships as an HF BF16 checkpoint, which the family must read with the reference's semantics. The family now detects the bf16 format before the shared formats, maps HF's model.language_model and lm_head names, refuses a mixed dtype by name, reads the conv weight (C, 1, K) as (C, K), and loads the trunk, final, q and k norms as 1 + w in fp32 by role (not by the mean heuristic), leaving linear_attn.norm as stored. The shard list of a bf16 checkpoint comes from its index, so the joint head stays out, and textBytes counts only the text tower and head.
A bf16 Qwen3.5 checkpoint keeps its projections as stored, so the HIP engine multiplies them through rocBLAS on its own stream, BF16 in and out with FP32 sums, in a planned 32 MiB workspace that enters planBytes. tf_embed_dense gains a table kind, read through load(), so the gather takes a BF16 table byte for byte; its kernel test, its contract rig (BF16 table, fp32 table bit-equal to the old gather, two planted controls) and its launch record come with it. The engine refuses bf16 with fp16 activations or more than one rank, turns round graphs off, counts only the text tower before loading, and the server refuses a bf16 checkpoint as decision-only until the readout slices land.
Gate 1 compares every final-norm row of a prompt against transformers, and C2's readout pieces need the rows of each piece. Engine.spanRows runs a span like advance but keeps fwd.span's rows, and advance now calls it. tf-qwen35-test hidden writes every prompt row's final-norm row through spanRows, piece by piece at a given cut, as a BF16 .npy; --layers instead writes each layer's residual rows, to find which layers an error grows in.
Gate 1 needs transformers' rows on the same records as the native run. clef_backbone_rows.py writes the 40 seeded synthetic records (10 per length bucket), runs the text model in BF16 on the GPU and in FP32 on the CPU, drives tf-qwen35-test hidden, and reports per bucket the pooled mean absolute error against FP32 for native and BF16, and every teacher-forced last-row top-1 difference with its margins; a non-finite reference voids the run. SOURCES pins torch, transformers and the checkpoint revision.
Blas.open resolved rocblas_set_workspace and the two workspace queries for every caller, so a loader that never sets a workspace, such as GLM-OCR, needed a rocBLAS that has them. The three calls now form their own table, resolved on the first useWorkspace or workspace call, which only the bf16 Qwen3.5 path makes; a rocBLAS without one is refused there by the call's name. A host test checks that each missing call is named and that open no longer asks for them.
212f687 to
9a48352
Compare
|
Thank you @laudney. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes. |
Part of #1360, closes #1952.
Why
Clef-Flash's joint-schema decisions run on its publisher's BF16 Qwen3.5-9B backbone. Serving the publisher's own weights, not a re-quantized copy, is what makes a benchmark against vLLM or SGLang meaningful and what lets someone adopt the model without asking which conversion they got. This PR is the backbone only: the readout requests and the head follow in the stacked PRs.
What
feat(quant): abf16projection format beside the MLX affine widths.feat(qwen3_5): HF BF16 tensor names, the conv layout and the norms of the release checkpoint.feat(hip): BF16 projections and embeddings through rocBLAS (hip.Blas.project), with the engine owning a planned 32 MiB rocBLAS workspace so the startup memory plan stays exact.fix(hip): look up the rocBLAS workspace calls on the BF16 path only:Blas.openresolves only the product calls;rocblas_set_workspace,rocblas_get_device_memory_sizeandrocblas_is_managing_device_memoryare resolved when the BF16 engine first sets its workspace, and a missing one is refused by name, so GLM-OCR and MLX, which share the loader, never need them.test(qwen35)andtools/clef_backbone_rows.py: dump final-norm rows natively and from transformers in BF16 and FP32 over a seeded 40-record corpus (10 per length bucket), then compare.Receipts
Gate 1 (rows): the native rows are 2–5% nearer FP32 than transformers' own BF16 rows in every bucket, and the teacher-forced top-1 through transformers' BF16 LM head matches on all 40 records. Single rows are not judged: full-attention layers amplify an input error near a tie of scores, and transformers BF16 has a few rows as far from FP32 as native has.
Gate 2 (seams): rocBLAS picks its Tensile kernel by the row count M, so a row's bits can change between prompt cuts of 512, 1,024 and whole; the same cut is byte-identical across repeats. A decision therefore never depends on the cut: a readout always starts at row 0 and never resumes from a kept state, its pieces are pinned to the 1,024-row chunk whatever else runs (#2047, with a control that fails without the pin), and chat on a BF16 checkpoint is refused, so nothing decodes from a kept prefix on this backbone. Byte-identical seams would need fixed-M calls or a pinned solution (upstream's
blas_fixed.zigfor GLM-OCR is that route), which is a follow-up if wanted.Checks:
zig build test -Dhip=trueon gfx1201 (ROCm 10.1),zig fmt --check, lean checker at the baseline.