Skip to content

[CLM 1/2] Checkpoint and typed decision scoring - #28

Merged
hsliuustc0106 merged 4 commits into
ThinkFlowLab:mainfrom
xiaoyu-xyz:clm-a-checkpoint
Oct 6, 2026
Merged

hsliuustc0106 merged 4 commits into
ThinkFlowLab:mainfrom
xiaoyu-xyz:clm-a-checkpoint

Conversation

@xiaoyu-xyz

@xiaoyu-xyz xiaoyu-xyz commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

CLM is the second model tracked in #9, and it decides differently in a way LAYA does not cover: the engine does not compute embeddings. A frozen Qwen3-8B encoder runs as its own process, and the engine owns everything after it. This is the half that can be checked without a GPU — the checkpoint, the head geometry, and the decision arithmetic.

Core code: 495 lines (config 76, weights 154, scoring 259, lib 6), within the 500-line budget. Tests and the export/oracle scripts are separate.

Two PRs, this one then #29:

core
this PR checkpoint, geometry, scoring 495
#29 the embeddings client, the request path, clm-run, and the comparison against CLM 681

The checkpoint is converted first

A CLM checkpoint is a torch.save dict, so it is a pickle and no non-Python reader can open it. recipe/clm/native/export_weights.py writes the tensors to safetensors with the head name as a prefix, and keeps cfg, hidden_size, projection_dim and logit_scale in the metadata. The published CLM_v0.1-8B.pt is 75 MB and holds the two heads, not the 8B encoder; the export is 16 tensors and 18.9 M parameters.

The decision

softmax(exp(logit_scale) * cos(state_head(s), action_head(c)) / temperature) over a question's candidates. Both heads are inp → [LayerNorm →] hidden → out with GELU, and both projections are L2-normalised before the dot product.

The three types differ only after the distribution exists, so they share one path: choice is the argmax key, noul is the true entry of a two-candidate distribution, and score is the expected level index. confidence is the top probability minus the mean of the rest, clamped, and 1.0 for a single candidate — the definition src/clm/schema.py uses.

The scale cap

exp(logit_scale) is capped at 100, as heads.py caps it. The published logit_scale is 4.6132, whose exponential is 100.82, so the cap binds — without it every probability is about 0.8 % off, and more where candidates are close.

I had this wrong until a real encoder disagreed with me. The CPU-side oracle could not catch it: head_oracle.py was written from the same reading of the format and made the same mistake, so both sides agreed and the test was green. It took compare_with_reference.py (in #29) comparing against CLM's own heads over real Qwen3-8B embeddings.

Checks

With the checkpoint exported:

python recipe/clm/native/export_weights.py CLM_v0.1-8B.pt /tmp/clm-export
python recipe/clm/native/head_oracle.py /tmp/clm-export /tmp/clm-export/head-oracle.json
CLM_EXPORT=/tmp/clm-export cargo test -p omni-clm -- --include-ignored

6 tests: 16 tensor conversion hashes against the export oracle, five decisions against head_oracle.py — an independent NumPy implementation of the same arithmetic — on hash-derived embeddings, the loader against three synthetic checkpoints that differ only in layernorm, a cfg that carries its geometry only at the top level, and a tied score label. Default cargo test needs no checkpoint (4 passed, 2 ignored).

The CI job's commands pass locally: cargo fmt --all --check, cargo clippy --workspace --locked --all-targets -- -D warnings, cargo test --workspace --locked, cargo build --workspace --release --locked.

@xiaoyu-xyz xiaoyu-xyz changed the title [CLM 1/3] Checkpoint and typed decision scoring [CLM 1/2] Checkpoint and typed decision scoring Sep 28, 2026

@hsliuustc0106 hsliuustc0106 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed commit 2422066a707dc5a39f08be0087d486f868a62c42.

[P2] Respect layernorm=false when loading hidden blocks. src/models/clm/src/weights.rs:138 unconditionally loads norms.0.weight/bias for depth=3. Config and head_tensors accept layernorm=false and require these tensors to be absent, so a valid no-LayerNorm checkpoint passes inventory validation then fails to load. Reproduced with a tiny synthetic safetensors checkpoint. Load the norm tensors only when cfg.layernorm is true. Default tests: 1 passed, 2 checkpoint-dependent tests ignored.

@xiaoyu-xyz

Copy link
Copy Markdown
Contributor Author

Fixed in b918ca2. load_head now reads norms.N.* only when cfg.layernorm is set.

The regression test writes three checkpoints — no LayerNorm, LayerNorm with the tensors, and one that declares LayerNorm and omits them — and the third is still rejected by the inventory. It fails on the previous revision.

@Levius-Fubuki Levius-Fubuki left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed b918ca2f5ce516d42fb6ffb08f49a429f58c2baa, including the full config/weights/scoring change and the prior LayerNorm finding.

The original LayerNorm finding is fixed: the synthetic cases accept a checkpoint without norms when layernorm=false, accept a complete normed checkpoint, and reject a checkpoint claiming norms but missing their tensors.

Fresh Linux/Rust 1.98.1 validation: formatting, strict all-target Clippy, workspace tests and release build passed. The workspace default run passed 15 tests (two CLM external-data tests skipped there). All four CLM tests subsequently passed with the published head checkpoint: 16 tensor FP32 hashes and the five NumPy head-oracle decisions matched. Checkpoint revision e939398d4556fcd9400c76fa8c5a513202f42b0a was verified against official SHA-256 b2b4a8c9c2d39263eff78a351eb909a342ce9b3bf21a3f07c1d1bf15f1c4eda5.

Changes requested for the metadata compatibility defect below; the score-label tie mismatch is a lower-priority additional finding. Both were reproduced with small CPU probes and compared with official CLM source pinned to bb42c6c5bf914fd449bed2f6ca65be80602cb1f7. No real Qwen/vLLM or GPU execution was performed. Existing merge conflicts also need resolution before merging.

);
let cfg = metadata.get("cfg").context("metadata has no cfg")?;
let mut head: HeadConfig =
serde_json::from_str(cfg).with_context(|| format!("parse cfg {cfg}"))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Apply top-level geometry fallback before deserializing required cfg fields

The following fallback promises to load exports whose cfg omits hidden_size/projection_dim, but both are required fields of HeadConfig, so deserialization fails before that fallback is reached. A metadata fixture with top-level hidden_size="4", projection_dim="2", format="clm-heads", finite logit_scale, and cfg={"width":3,"depth":3,"activation":"gelu","layernorm":false,"residual":false} returns missing field hidden_size. The supplied exporter can produce this layout: it preserves cfg while obtaining dimensions from the checkpoint's top-level entries. Resolve the dimensions into the config before deserialization (or deserialize optional fields and then validate the resolved dimensions), and add a regression for both absent cfg fields.

Comment thread src/models/clm/src/scoring.rs Outdated
Answer::Noul { noul } => if *noul >= 0.5 { "true" } else { "false" }.to_string(),
Answer::Score { probabilities, .. } => probabilities
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] Keep the first maximum when score labels tie

Iterator::max_by returns the last equal maximum, whereas the upstream schema.label_of keeps the first key. For score probabilities [("0", 0.5), ("1", 0.5)], the current public Answer::label() returns "1" and the pinned upstream returns "0". Equal probabilities can arise from duplicate score-level descriptions, so this changes the discrete label despite identical probabilities. Preserve first-maximum tie breaking and add a tie case. This affects the label helper; it does not change the serialized expected score.

@xiaoyu-xyz

Copy link
Copy Markdown
Contributor Author

Fixed in e1a1211, and the stack is rebased on main (58b8cbe), so the conflicts are gone.

  • Geometry fallback. It now runs before deserialization. Both fields are required by HeadConfig, so the old fallback never ran — a cfg without them failed to parse first. The test covers a cfg that omits both (loads), one that carries them, and one that omits them with no top-level copy (still an error).
  • Label ties. Answer::label keeps the first maximum now, as max(p, key=p.__getitem__) does; max_by kept the last, so [0.5, 0.5] named level 1 where the reference names 0.

Reverting either fix turns its test red. Strict Clippy, workspace tests and the release build pass on 58b8cbe locally.

@hsliuustc0106 hsliuustc0106 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Independent local review — CLM 1/2

Verdict: changes requested — one convention finding; the model work itself is verified and impressive.

[Medium] Test bodies live in src/models/clm/tests/ — a crate-local tests/ directory under src/. CONTRIBUTING prohibited this at this PR's own base ("Do not add inline test bodies or crate-local tests/ directories under src/… the test implementation stays under root tests/"), and current main follows it (tests/laya, tests/cua_s1, tests/open_jev, tests/runtime, tests/qwen3_5). The tests do run via cargo autodiscovery, so this is placement, not breakage — please move them to repo-root tests/clm/ with [[test]] registration or the #[path] convention.

Verified: the exp(logit_scale) cap (scale() = exp(4.6132).min(100) = 100, not 100.82) is correctly implemented and documented as binding; weights loading validates format/tensor names/shapes/dtypes; scoring preserves first-maximum tie semantics (matching Python max), name-based noul lookup, clamped top-minus-mean confidence, bounded temperature. Locally at head e1a1211: cargo test -p omni-clm → 4 passed / 2 ignored (checkpoint-gated, disclosed); clippy -D warnings clean. The exported-checkpoint gates (hash oracle, five decisions vs head_oracle.py) remain author-reported — no checkpoint on this machine.

Local reviewer report per the repo review skill; reflects head e1a1211 only.

@hsliuustc0106 hsliuustc0106 mentioned this pull request Oct 5, 2026
2 of 4 tasks
xiaoyu-xyz pushed a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
The same rule `src/models/clm/tests/` broke in ThinkFlowLab#28 applies here, and to the
inline test in `embedding.rs`: CONTRIBUTING keeps test bodies in the
repository-level `tests/` tree, and for a private item only the `#[cfg(test)]`
and the module path may stay in `src/`. `HttpEncoder::body` is private, so that
is the `#[path]` form, with the body in `tests/clm/embedding.rs`.

Found in review.
@xiaoyu-xyz

Copy link
Copy Markdown
Contributor Author

Moved to the repository-level tree in f4286a9: src/models/clm/tests/checkpoint.rs → tests/clm/checkpoint.rs, registered with an explicit [[test]] path in the crate manifest, matching src/models/laya/Cargo.toml.

#29 does the same for its own tests, and for the one test body that was inline in embedding.rs — HttpEncoder::body is private, so that is the #[path] form CONTRIBUTING allows, with the body in tests/clm/embedding.rs.

src/models/clm/ no longer has a tests/ directory. cargo test -p omni-clm finds all three targets: 1 + 6 + 7.

xiaoyu-xyz added 4 commits October 5, 2026 15:10
CLM is the second model tracked in ThinkFlowLab#9, and it decides differently in a way LAYA does not
cover: the engine does not compute embeddings. A frozen Qwen3-8B encoder runs as its own
process, and the engine owns everything after it. This is the half that can be checked
without a GPU -- the checkpoint, the head geometry, and the decision arithmetic.

A CLM checkpoint is a torch.save dict, so recipe/clm/native/export_weights.py converts it
first: tensors to safetensors with the head name as a prefix, and cfg, hidden_size,
projection_dim and logit_scale into the metadata. The published checkpoint is 75 MB and
holds the two heads, not the 8B encoder; the export is 16 tensors and 18.9 M parameters.

- config reads the head geometry from the metadata and checks it.
- weights lists the expected inventory from that geometry, checks shapes before reading,
  and loads FP32.
- scoring projects a state and each candidate, L2-normalises both, scores them by cosine
  under exp(logit_scale), softmaxes across the question's candidates, and assembles
  choice, noul and score. The three types differ only after the distribution exists, so
  they share one path. confidence is the top probability minus the mean of the rest,
  clamped, and 1.0 for a single candidate, which is the definition src/clm/schema.py uses.

exp(logit_scale) is capped at 100, as the reference caps it in heads.py. The published
logit_scale is 4.6132, whose exponential is 100.82, so the cap binds: without it every
probability is about 0.8 % off.

Checks. head_oracle.py is an independent NumPy implementation of the same arithmetic, and
tests/checkpoint.rs checks 16 tensor conversion hashes against the export oracle and five
decisions against head_oracle.py. Default cargo test needs no checkpoint. fmt, clippy -D
warnings.
`head_tensors` lists `norms.N.*` only when `cfg.layernorm` is set, so a
`layernorm: false` checkpoint is complete without them — but `load_head`
read them unconditionally. A valid checkpoint passed the inventory check
and then failed to load.

The new test writes both shapes plus one that declares LayerNorm and
omits the tensors, and checks the loader accepts the first two and
rejects the third.

Found in review.
…labels

- `Config::from_metadata` applied the top-level `hidden_size`/`projection_dim`
  fallback *after* deserializing `cfg`, but both are required fields, so a
  `cfg` that omits them failed to parse and the fallback never ran. The
  geometry is now filled in before deserialization, and a `cfg` that omits it
  loads while one that omits it with no top-level copy is still an error.
- `Answer::label` used `max_by`, which keeps the *last* of several equal
  values. `schema.label_of` is `max(p, key=p.__getitem__)`, which keeps the
  first, so a tie in a score distribution named a different level.

Found in review.
CONTRIBUTING puts every test body in the repository-level `tests/` tree and
asks for explicit `[[test]]` registration, which is what `src/models/laya`
does. `src/models/clm/tests/` was a crate-local directory under `src/`, which
that rule prohibits; the tests ran either way, so this is placement rather than
breakage.

Found in review.
@Levius-Fubuki

Copy link
Copy Markdown
Collaborator

Rechecked f4286a9: checkpoint tests are correctly registered under root tests/clm, and the model implementation is unchanged. Rust checks pass. Please resolve the remaining main conflicts and recheck the rebased head; checkpoint-dependent tests were not rerun here.

xiaoyu-xyz pushed a commit to xiaoyu-xyz/system1-omni that referenced this pull request Oct 5, 2026
The same rule `src/models/clm/tests/` broke in ThinkFlowLab#28 applies here, and to the
inline test in `embedding.rs`: CONTRIBUTING keeps test bodies in the
repository-level `tests/` tree, and for a private item only the `#[cfg(test)]`
and the module path may stay in `src/`. `HttpEncoder::body` is private, so that
is the `#[path]` form, with the body in `tests/clm/embedding.rs`.

Found in review.
@xiaoyu-xyz

Copy link
Copy Markdown
Contributor Author

Rebased onto main (7f39ac4) and rechecked the new head, 9650e2a.

The only conflict was the workspace members line in the root Cargo.toml; it now
carries main's list with "src/models/clm" added. git range-diff against f4286a9
shows the three commits after the first are unchanged, and the first differs only in
that line, so nothing the review read moved.

Checks on the rebased head, locally:

  • cargo fmt --all --check clean
  • cargo clippy --workspace --locked --all-targets -- -D warnings clean
  • cargo test --workspace --locked 86 passed, 0 failed

The CI job for 9650e2a is green, release build included.

The checkpoint-dependent tests you could not rerun, with CLM_EXPORT pointed at the
converted checkpoint:

cargo test -p omni-clm --locked -- --ignored
test decisions_match_the_reference_implementation ... ok
test every_tensor_conversion_matches_the_oracle ... ok
test result: ok. 2 passed; 0 failed

The pre-rebase head was f4286a9, so

git range-diff f4286a9~4..f4286a9 9650e2a~4..9650e2a

reproduces that comparison; it prints the first commit with only that line changed and the
other three as =.

@hsliuustc0106
hsliuustc0106 merged commit ae86032 into ThinkFlowLab:main Oct 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants