Repository navigation
Add jev-style family: Jev-Style-2B-Decision-v3 (non-causal block attention + verdict readout) - #77
Conversation
Port of Jev-Style-2B-Decision-v3's macjev-render-v2-long-options input and verdict-slot layout from the model's own jev_style_decision_gguf.py shared core: State/Question/Options render, short + overflow (catalogue+rubric) forms, the per-option " ->" slots, 2048-token attention blocks, and softmax(scores/T). Unit-tested against the reference render. Engine decode wired in a follow-up. Refs ollaya-dev#76
Add the engine half of the jev-style family to the llama runner: - FFI bindings llama_memory_seq_cp and llama_synchronize; - data-driven context params (attention_type, n_ubatch, n_seq_max, n_outputs_max) on LlamaSettings so a jev-style model loads a non-causal, 2048-ubatch, 2-seq context with n_outputs_max slots; - Layout::JevStyle, its prepare arm (validating the yes/no/arrow readout tokens against the GGUF), and run_jev_style: decode the state into sequence 0 once, then per question seq_cp 0->1, decode its blocks into sequence 1 reading logits only at the verdict slots, score = logit(yes) - logit(no), seq_rm(1). The global temperature is applied by the existing calibration layer. Builds; render layer unit tests pass. Refs ollaya-dev#76
- convert/ollaya_convert/families/jev_style/ref.py: the render-v2 Python reference, byte-parity-tested against the model's own jev_style_decision_gguf.py (choice, score, noul and the overflow catalogue+rubric form: ids, slots, blocks all match). - llm_common/export_llama.py: a JevStyle exporter that writes decision.json (layout, the yes/no/arrow readout token ids read from the GGUF, and the non-causal llama settings via llama_extra) and calibration.json (the one global temperature); the causal logit goldens are skipped (jev-style reads verdict slots, which need the author's jev-score-v2, not the llama-server plan). - catalog.py: the jev-style:2b entry (Q4_K_M, repo@283b9eb). Refs ollaya-dev#76
- cargo fmt --all (jev_style.rs, ollaya-decision/lib.rs, llama/mod.rs) - decode_block: merge lo/hi into a Range<usize> so it has 7 arguments, not 8, satisfying clippy::too_many_arguments; update both call sites - llama settings_default test: add the new attention_type / n_ubatch / n_seq_max / n_outputs_max fields to the struct literal, which fixes the missing-fields error (E0063) when building with the mlx feature
cobanov
left a comment
There was a problem hiding this comment.
@mirfan-cof, thanks. This is the right approach: the render and the scorer ported onto the runner's libllama, with no new native code. I checked it against jev_style_decision_gguf.py and jev_score_v2.cpp at 283b9eb:
- The render, slots and blocks match the author's
Renderer.assemble. - The new context fields keep today's values for every other family. At b11146,
llama_context_default_params()gives n_ubatch 512, n_seq_max 1, attention UNSPECIFIED, n_outputs_max 0 and n_outputs_max_per_seq 1. - The
export_llama.pychange is a no-op for the existing layouts.
What has to change:
-
CI is red.
cargo fmt --all --checkfails.- clippy fails on
decode_block, which has 8 arguments (llama/mod.rs:772). Pass the block as a slice plus its start position rather than adding an#[allow]. - The test initializer at
llama/mod.rs:1837doesn't compile, because the newLlamaSettingsfields are missing.
Run
cargo fmt --all && cargo clippy --workspace --all-targets --locked -- -D warnings && cargo test --workspacebefore pushing. -
Use jev-score-v2's context.
jev-score-v2setskv_unified = true, but the runner hard-codesfalse(llama/mod.rs:1678). With two sequences that has two effects:- Each sequence gets only half the cells (
n_ctx_seq = n_ctx / 2). - Every
seq_cpbecomes a copy of the whole KV buffer.
n_ctxalso comes fromexport_llama.py's default of 8192, becauseJevStyledoesn't set it. So sequence 1 has 4,096 cells, but the render accepts 25,600 tokens: any input over 4,096 tokens fails insidellama_decodeas a 500 instead of a 422. The fix is the author's values:n_ctx32768 andkv_unifiedtrue (aLlamaSettingsfield like the others). Also reject an input the context can't hold before decoding it. - Each sequence gets only half the cells (
-
Flash attention on the CPU.
jev-score-v2turns it off on the CPU backend, where the author measured a precision loss, and uses auto on a GPU. The runner leaves it on auto everywhere. The CPU is the machine #76 is about, so follow the reference's setting per device. -
Validate the context at load. The prepare arm (
llama/mod.rs:578) checks the readout tokens but not thellamasettings. Two ways it goes wrong:- A
decision.jsonwithoutattention_type: 1silently scores with causal attention. - One with
n_ubatchbelow 2048 hits llama.cpp'sGGML_ASSERT(n_ubatch >= n_tokens)and aborts the runner.
decode_blockshould also check each block against the context'sn_ubatch, asjev-score-v2does, not againstBLOCK(llama/mod.rs:783). - A
-
Use the shared question parser.
jev_style.rsparses questions again and rejects input the rest of Ollaya accepts:- choice criteria given as a list of labels (
jev_style.rs:114). The author's ownmake_questiontakes a list too and turns it into{name: None}; - noul keys in another case (
"True").
Start from
question.rs. Keep the author's own limits (2 to 10 score levels, instructions as a non-empty string) as documented 400s. - choice criteria given as a list of labels (
-
Non-finite scores. The author's runtime refuses NaN and inf scores. Here they go on to the calibration (
llama/mod.rs:734,751). Return an error instead.
What's needed to merge (docs/model-lifecycle.md, stages 1 to 3):
-
Goldens from jev-score-v2. Build
jev_score_v2.cppunchanged against llama.cpp b11146. That is 27 commits after the 441df11 it was tested with, and libllama is unchanged in between.- Inputs: the shared set, plus cases for this layout: a multi-block state, a 25,600-token input, a 25,601-token reject, and catalogue overflow with about 150 options.
- Per question, record: the token ids, blocks and slots, the
[yes, no]logits (return_logits) and the scores. - One file per device, as for winnow.
ADR 0003 says a
seq_cpsplit needs its own reference, and this is it. Put the Python side infamilies/jev_style/goldens.py, or make theJevStyleexporter write the goldens instead of returning early. -
A gate.
parity_llamacallsmodel.encode, which errors for this layout. Either add aparity_jev_styleexample or giveparity_llamaa jev-style branch. It checks identical ids, slots and blocks, every decision the same and scores within 1e-3, and prints onerecord:line. We'll run CUDA on the RTX 4090. Your box can do the CPU run, recorded as a community machine. -
A render test against the author's tokenizer. The unit tests use a character tokenizer, so they don't show the byte parity the PR description mentions. Add a test under
convert/teststhat renders the shared set withref.pyand with the author'sRenderer(tokenizer/tokenizer.jsonat the pinned commit) and compares ids, slots and blocks, including the overflow form and the budget edges. -
The tag. Ship Q8_0 as
jev-style:2b(andlatest). The card puts Q4_K_M at 95.7% top-1 agreement with fp32 and says it is noisier on long inputs. Q4_K_M can be2b-q4_k_m. -
Docs.
docs/families/jev-style.mdneeds:- a Parity section with the measured numbers;
- the card's training-data notes, as
jevk5.mdhas them; - a Limits section: 25,600 tokens, 2 to 10 score levels, string instructions.
The
paritytext incatalog.pymust give measured numbers, not "pending".
After the gate passes, I'll do the site overlay, the library page and the bench run (stages 3 and 4).
Responds to the review on ollaya-dev#77 (items 1-6): 1. decode_block takes the already-sliced block plus its start position (6 args, no #[allow]); both call sites pass the slice. 2. Use jev-score-v2's context: a kv_unified LlamaSettings field (true for jev-style, so sequence 1 gets the whole context and seq_cp is not a full-buffer copy); the JevStyle exporter pins n_ctx 32768 and kv_unified; run_jev_style rejects an input larger than the context before decoding (a 422, not a 500 from inside llama_decode). 3. Flash attention off on the CPU for the non-causal block model (jev-score-v2 measured a precision loss there), auto on a GPU. 4. Validate the context at load: attention_type must be 1 and n_ubatch >= 2048; decode_block checks each block against the context n_ubatch, not the fixed BLOCK. 5. Render from the shared question parser (crate::question::Question), so a choice given as a list of labels and case-variant noul keys are accepted like the rest of ollaya; the author's limits (2..=10 score levels, a non-empty string instruction) stay as 400s. ref.py matches. 6. Reject a non-finite verdict score (NaN or infinite) instead of passing it to the calibration.
|
Thanks for the thorough review, @cobanov — this is exactly the kind of feedback I was hoping for. I've pushed
One thing I'd flag for the goldens: with On 7–11 (goldens from |
|
@mirfan-cof, thanks. I approved the workflow for I went through items 1–6 against the review:
So what's left is the item 4 check and the tests above, then 7–11 as you planned: goldens from |
…inputs Follow-up to the second review on ollaya-dev#77: - Item 4: the prepare arm now also refuses a decision.json whose llama.n_seq_max is below 2, next to the attention_type and n_ubatch checks. With one sequence, llama_memory_seq_cp(0, 1) trips llama.cpp's GGML_ASSERT and aborts the runner. - Item 5: unit tests for the newly accepted inputs (a choice as a list of labels, with duplicates collapsing; mixed-case noul keys) and for the author's limits (1 and 11 score levels refused, 2 and 10 accepted; object, list, number and blank instructions refused). The same cases and expected texts run against ref.py in convert/tests/test_jev_style_ref.py, so the two renders stay in step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…okenizer, Q8_0 tags Review items 7-10 on ollaya-dev#77: - goldens.py: the author's Renderer (jev_style_decision_gguf.py) scored by jev_score_v2.cpp, unchanged, built against the b11146 headers and linked to ggml-org's stock b11146 release libraries (load_backends.c loads their CPU backend variants before main). Ollaya's shared request set plus this layout's cases: several state blocks, a 150-option catalogue overflow, inputs of exactly 25,600 and 25,601 tokens. Per question: ids, slots, blocks, [yes, no] logits, scores; one file per device. - parity_jev_style: identical ids, slots, blocks and option names, every decision the same, verdict scores within 1e-3; prints a record: line. parity_run records GGUF families listed in GGUF_RECORDED (jev-style), and results/machines.json gains the community CPU machine. - The render now uses the author's tokenizer.json (the package's tokenizer layer, passed to the llama runner like --mmproj): llama.cpp's Qwen3.5 pre-tokenizer splits some scripts differently (the shared set's Hindi state is 26 tokens there, 40 with the author's tokenizer). - convert/tests/test_jev_style_render.py: ref.py against the author's Renderer on the pinned tokenizer.json, shared set plus the layout cases. - Tags: jev-style:2b (and latest) is Q8_0; Q4_K_M is 2b-q4_k_m. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review item 11 on ollaya-dev#77, and the stage-2 record for item 8: - results/runs/2026-10-26-parity-communityepycmilan-cpu.json: parity_run on community-epyc-milan, Ollaya at cf1fa64. jev-style:2b (Q8_0) and jev-style:2b-q4_k_m: 582 questions each, identical ids/slots/blocks, 582/582 decisions, verdict scores within 4.4e-16 of jev-score-v2. - docs/families/jev-style.md: Parity (the numbers above, what the set covers, what was rejected and what was not run), the tokenizer, Limits (25,600 tokens, 2 to 10 score levels, string instructions) and the card's training-data notes. - catalog.py: the parity text gives the measured numbers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@cobanov, thanks for the second pass. Everything you listed is now pushed: the item 4 check, the tests for item 5, and items 7–11. Commits
4. 5. Tests.
A bug the goldens found: the tokenizer. On the shared set, the runner's ids differed from the author's on non-Latin text. For example, the Hindi state is 26 tokens with the GGUF vocabulary and 40 with the author's The fix: the jev-style tags ship the author's
The GGUF vocabulary is now used only to index the logits. With this change, every id matches. 7. Goldens. The scorer is compiled against the b11146 headers and linked to ggml-org's stock b11146 release libraries, the same binaries the runner loads. Those builds load their CPU backend variants at run time, and
A wire question becomes the author's 8. Gate.
It prints one
This also confirms what you said about The CPU is an AMD EPYC Milan cloud VM with 8 vCPUs (the AVX2 backend), recorded as a community machine. I haven't run CUDA, Metal or Windows, and haven't measured latency. That's yours on the RTX 5090. 9. Render test.
The pinned files are fetched once into the package cache, with sha256 checks, and the test skips without network. 10. Tags. 11. Docs.
The Two corrections to my previous comment:
|
…xt to GGUF_RECORDED) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nGz6RQQsBfLJAGByzb1Bn
goldens.py imports numpy at module level, so CI's packaging job (no ML dependencies) failed to import the test module instead of skipping it. Same skipUnless pattern as test_fp16.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nGz6RQQsBfLJAGByzb1Bn
|
@mirfan-cof, thanks. This is thorough work, and the tokenizer bug the goldens caught is a real find. I went through the third round against items 1–11:
I pushed two small commits to your branch:
Nothing else is needed from you. What's left is on our side: CUDA goldens and |
Goldens from jev-score-v2 on the same GPU (the CUDA 13.4 backend of the b11146 libraries), then parity_run --machine rtx-5090 --device cuda: both tags 582/582 decisions, identical encodings and the same 17 rejections, verdict scores bit-identical. The family doc and the catalog's parity text now name both devices. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nGz6RQQsBfLJAGByzb1Bn
|
@mirfan-cof, CUDA parity passes on our RTX 5090, so I'm merging. Setup: goldens from
The record is |
Adds a
jev-stylefamily for chaoliangUNSW/Jev-Style-2B-Decision-v3 (Apache-2.0), requested in #76.Unlike
jevk5/llm-logits, Jev-Style is not a last-token label-logit model: it uses a plain-text "render v2" input (no chat template, no system prompt, no answer letters) and a verdict readout —logit(" yes") - logit(" no")at a per-option->slot — read under non-causal block attention. So this needs a small engine addition, not just a layout recipe. It is ported from the model's own self-contained runtime (jev_style_decision_gguf.pyshared core) and scorer (jev_score_v2.cpp), which ship in the GGUF repo.What is in this PR
crates/ollaya-decision/src/jev_style.rs— the render-v2 layout:State/Question/Optionsrender, short + overflow (catalogue+rubric) forms, the per-option->verdict slots, the 2048-token attention blocks, and option/name ordering. Unit-tested against the reference render (cargo test -p ollaya-decision jev_style).crates/ollaya-runner:llama_memory_seq_cpandllama_synchronize(additive toApi; the context-param layout check is untouched);LlamaSettings(attention_type,n_ubatch,n_seq_max,n_outputs_max, all defaulting to today's causal values) so a jev-style model loads a non-causal, 2048-ubatch, 2-sequence context;Layout::JevStyle+ its prepare arm (validating theyes/no/->readout tokens against the GGUF);run_jev_style: the state decodes into sequence 0 once, then per questionseq_cp(0->1), decode its blocks into sequence 1 with logits only at the verdict slots,score = logit(" yes") - logit(" no"),seq_rm(1). The one global temperature (0.828) is applied by the existing calibration layer.docs/families/jev-style.md.Verified
Builds clean (
cargo build -p ollaya), render unit tests pass.It runs on the real
Jev-Style-2B-Decision-v3-Q4_K_M.ggufon CPU and discriminates correctly (ad-hoc lawful/misconductchoicequestions):Not yet done (happy to do, and would value your steer)
convert/ollaya_convert/families/jev_style+ acatalog.pyentry) that produces the model package / publishes it to the registry.jev-score-v2: the above validates functional correctness but not float-parity to the reference scorer. Note Ollaya pins llama.cpp b11146 whilejev_score_v2.cppwas tested against 441df11, so the non-causal kernels should be parity-checked on your infra.n_outputs_max_per_seqhandling here may also want your eyes.Refs #76.
🤖 Generated with Claude Code