Repository navigation
Conversation
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.
On a backend with prefill_many and many_rows above zero, the batched admission took two short embeddings into one addStreams pass and then delivered them; nothing called the embedding completion, so neither request ever finished. A prompt-only stream (an embedding for now) now always goes through launch, which fills it and completes it, and a test on such a fake reproduces the hang without the fix.
A readout is a non-generating request whose answer, BF16 logits per option, the backend computes from the prompt's final-norm rows. The lane core gets the Spec and Info types with their validation, a Request and Stream field, one backend hook, readout_finish, and a completion that mirrors the embedding one: logits preset to NaN, non-finite output refused, one release on success, failure or cancel. Embeddings and readouts now share a prompt-only predicate, so both skip the warm pass, the lone driver, batched admission, the prompt cache, the repeat log and the Recall record. An engine that reports readouts refuses every other request.
A readout engine needs the same borrowed-storage wait as embeddings: submit, poll for the terminal event, cancel once when the client is gone and keep waiting until the engine is done with the request. That loop and its sink move from the embedding route into one module both use, with no new timeout. The server now refuses to start an engine that reports readouts beside text generation or embeddings, and such an engine answers every POST route except System One and tokenize with a 400 that names POST /v1/systemone.
Gate 2 compares readout rows across runs that must cut the prompt at the same rows, and the lane rule picks 1,024 or the policy's fill rows depending on what else decodes. Options.cut_rows fixes the piece size for tests and checks (0 keeps the rule), and the engine grows prompt_rows to hold one such piece, so a whole-prompt cut sizes its prompts arena for it.
The lane core calls readout_finish only, so the family owns a readout stream's state: a Readout interface (open, consume, finish, close) that C3's joint schema head will implement, held by the lane backend. begin opens a stream's state and free closes it, so every release path frees it; a readout starts at row 0 with no cuts and never asks the prefix tree. readoutPiece feeds each piece's final-norm rows from spanRows to the state, and reach honours the test-only cut, which a recorder can list. No implementation ships, so the engine still reports no readouts.
A readout stream's answer comes from its rows, not from a token, so the shared prompt step sends each of its pieces, the last one too, to the family's readoutPiece: the last piece runs no LM-head projection, draws nothing and copies no last row, then ends the fill. A cut on the way keeps no state. The vtable gains readout_finish for a family that declares readoutFinish, and both are checked with hasDecl, so Nemotron and Flash Next are unchanged.
The generic HIP adapter builds one lane host Info for Qwen3.5, Nemotron and Flash Next. It now takes readouts from the family's Loaded when that type has the field, and turns text generation off only when they are set, so every other family keeps its Info, which a host test checks for each registered family. Qwen3.5's Loaded reports what its lane backend computes, still none, and the backend refuses a readout on more than one rank, since a follower's final piece projects.
Gate 2a for C2 needs the rows a readout consumes to match whatever runs beside it. tf-qwen35-test readout installs a test-only readout that downloads every piece's final-norm rows, then runs one readout alone, four together with a piece of each in turn, one released after its first piece, and one after that, all at the same fixed cut. It fails unless the pieces cover the prompt from row 0 at that cut and every copy and its pieces equal the first; an error still releases each open lane while the readout's states live. It opens the engine directly, so the server's BF16 refusal stays.
|
Thanks @laudney. This is a clean middle layer. Every shared lane change is gated on the request kind, so text generation on every existing family takes the same path as before. The HIP Info test that checks every registered family is a nice touch. Before this lands, could you:
The embedding fix in the first commit can land on its own whenever you like. |
rocBLAS picks its BF16 kernel by row count, so a readout's rows could change bits if its pieces followed the lane rule, which shortens a prompt's pieces to the policy's fill rows while another stream decodes. reach now gives a readout the 1,024-row chunk whatever the other lanes do, unless the test cut is set; a host test checks this beside a decoding lane, and tf-qwen35-test readout takes --cut 0 to run the serving rule, with its together phase now beside a stream that decodes.
Device receipt |
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.
|
Thanks @laudney, that's everything we asked for. The pin in One small thing: the body's table says "16K prompt", but the posted runs are 3,000 rows. Could you say which length the row covers, or add the 16K run? We don't have an AMD GPU, so your gfx1201 readout check and control are the device evidence. This can land after #2046. |
|
The table row now names both lengths. The 16K run, on the same build as the receipt above (branch tip |
Main's loaded-group receipts (100363c) set `benchmark_ranks` in the HIP lane host's Info literal, which this branch had moved into `hostInfo`; the merge passes the group's world size to `hostInfo` as a parameter so the whole Info stays in one function, and the Info test now also checks a two-rank group.
|
Merged main in cf13b0b: main's loaded-group receipts (100363c) set |
|
Thanks @laudney. The 16K run and the table naming both lengths close the last point, and the rocBLAS workspace commit now sits in #2046 where it belongs. Your gfx1201 receipts are the device evidence here, since we have no AMD GPU. One small thing for the rebase onto #2046: the |
readoutrequests: prompt rows in pieces under the shared fill policy, then a family completionprefill_manybackendaddStreamspass and never called their completion, so neither finishedlaunch, which fills and completes it (fix(lanes), first commit; a fake reproduces the hang)POST /v1/systemoneStacked on #2046 (the BF16 backbone). Part of #1360. This answers the #1361 review ask that decision prompts join the shared rounds instead of a side path.
What
fix(lanes): launch prompt-only jobs alone: on a backend withprefill_manyandmany_rowsabove zero, batched admission took two short embeddings into oneaddStreamspass and delivered them without their completion, so neither request finished. It can land on its own.feat(lanes): add readout requests: areadoutSpec on the request (question and option spans), validated in the lane core, admitted like embeddings, filled inmax_rowspieces underfill.run, cancelled like any job, completed by the family when the last piece is in.feat(server): share the prompt-only wait: embeddings and readouts share one submit-and-wait helper.feat(qwen3_5)/feat(hip): the HIPReadoutinterface (open / consume / finish / close), readout prompts through the shared prefill step, and the engine'sreadoutsin its Info (text_generationis off when they are on).test(qwen35): the gate 2a readout rows check (alone, together, after a cancel).Only a fake
Readoutis wired here; the joint schema head that implements it comes in the next PR.Checks:
zig build test -Dhip=trueon gfx1201 (ROCm 10.1),zig fmt --check, lean checker at the baseline.