fix(config): an absent field is None, not a plausible number - #77
Conversation
CireSnave's standing policy, recorded verbatim in CLAUDE.md section 2: "Make the fields a supported format may not supply `Option<T>`. That way it doesn't matter if they are unable to supply them or someone previously writing the file chose not to write those fields to the file, either way MLMF just works." Eight `ModelConfig` fields become `Option<T>`: intermediate_size, max_position_embeddings, dropout, layer_norm_eps, attention_dropout, activation_function, rope_theta, tie_word_embeddings. The discriminator is sharper than "documented default": a specification that says what ABSENCE MEANS licenses a value; one that merely offers a convenient starting number does not. Every constant removed here failed that test -- their own comments called 4096 a "Common LLaMA max length" and hidden_size * 4 what SwiGLU "typically" uses. `num_key_value_heads` passes it, because absent genuinely means one KV head per query head, so it stays concrete. On the HuggingFace path this was not missing information but DESTROYED information: serde already declared twelve of these `Option`, and `to_model_config` collapsed them one layer later with nine `unwrap_or` calls. Eight are gone; the ninth is the licensed one. Consumers, where an absent input must not become a fabricated one: - `estimate_memory_usage` returns `Option<MemoryEstimate>`. Every assumption it makes errs toward MORE memory, never less -- a finite domain (gating, tying) takes its worst case, an unbounded integer (intermediate_size) refuses. GGUF requires feed_forward_length, so every GGUF model still estimates. - `validate_memory_requirements` refuses rather than returning Ok. A check that could not run must not report success. - The model card renders "not declared" instead of a number, and emits no "7b" size tag from a count it could not compute. - `is_gated_ffn` returns `Option<bool>`. Its sibling `ffn_hidden_size` had two arms returning the same expression, so the gated branch was dead and its comment described behaviour the code did not have; the branch is removed rather than "fixed", which would have meant deciding a consumer's question. ONNX: `max_position_embeddings` was seeded to 2048 and never assigned anywhere, so every ONNX model reported a 2048 context length unconditionally. Now None. The remaining GPT-2 seeds are #76. model_config_literals.rs: `is_literal` now sees through `Some(..)`. Without it this commit would have silently retired that guard -- every value it inspects changed shape at once, and `Some(10000.0)` reads as a call, which the scanner permits. Verified by control: with the unwrap the planted `rope_theta: Some(10000.0)` is caught; without it the same sabotage passes. Its non-vacuity assert now counts model fields PARSED rather than literals FOUND, because the old form became unsatisfiable once the defect was gone -- and deleting the guard, which its own message advised, would have retired the only mechanical check on a class that is still live in onnx_import.rs. Verified: fmt --all clean; 110 lib tests; 29 doc tests; all 8 gated crates test + clippy -D warnings clean; 0 of 313 root-crate clippy warnings land on a line this change authored (control: config.rs has 303 added lines and 4 warnings, none overlapping). Sabotages: reintroducing the rope_theta fallback reddens both new tests, and so does an orthogonal one on intermediate_size. The corpus test was made to speak -- it reports `llama.rope.freq_base, got 100000` from a real checkpoint, where the removed default was 10000. Closes #48 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MdVuiraXRfDHQ227cjBt51
There was a problem hiding this comment.
Sorry @ciresnave-bot, you've used your own review budget of 250,000 diff characters for the last 7 days.
You can request another review in 5 days and 2 hours by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideThe PR removes unsupported configuration defaults and carries absence as Sequence diagram for preserving absent configuration fieldssequenceDiagram
participant File as Model file
participant Parser as Format parser
participant Converter as Format converter
participant Config as ModelConfig
participant Consumer as Consumer
File->>Parser: parse configuration
Parser->>Converter: optional field or None
Converter->>Config: to_model_config(architecture)
Config-->>Consumer: declared value or None
alt intermediate_size absent
Consumer->>Config: estimate_memory_usage()
Config-->>Consumer: None
else intermediate_size declared
Consumer->>Config: estimate_memory_usage()
Config-->>Consumer: Some(MemoryEstimate)
end
Flow diagram for absence-aware consumersflowchart LR
Config["ModelConfig with Option fields"] --> Estimate{intermediate_size available?}
Estimate -->|No| NoEstimate["No MemoryEstimate"]
Estimate -->|Yes| Memory["estimate_memory_usage returns Some"]
NoEstimate --> Validation["validate_memory_requirements refuses"]
Memory --> Validation
Config --> Params{parameter count computable?}
Params -->|No| Card["Model card: not declared; no size tag"]
Params -->|Yes| Card2["Model card: render count and size tag"]
Config --> Sequence{max position declared?}
Sequence -->|No| Length["Max sequence length: not declared"]
Sequence -->|Yes| Length2["Render declared length"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 9 |
| Duplication | 2 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Closes #48. Implements CireSnave's standing policy, recorded verbatim in
CLAUDE.md§2 and quoted in full there:The discriminator, which is sharper than "documented default"
A specification that says what ABSENCE MEANS licenses a value. One that merely offers a convenient starting number does not.
That test decides every field, and it is checkable rather than a matter of taste. The constants removed here fail it in their own comments, verbatim before removal:
"Common" and "typically" are statements about what models usually do. The HF config format documents no meaning for an absent key at all.
num_key_value_headspasses the test and therefore stays concrete: absent genuinely means one KV head per query head — ordinary multi-head attention, which is what GQA degenerates to. Turning it intoNonewould discard a fact the format actually gives.Eight fields become
Option<T>intermediate_size·max_position_embeddings·dropout·layer_norm_eps·attention_dropout·activation_function·rope_theta·tie_word_embeddingsserdealready declared twelve of theseOption;to_model_configcollapsed them one layer later through nineunwrap_orcalls. Eight are gone, and the type is what keeps them gone. The ninth is the licensed one above.Consumers, where an absent input must not quietly become a fabricated one
estimate_memory_usagereturnsOption<MemoryEstimate>.feed_forward_length, so every GGUF model still estimates.validate_memory_requirementsrefuses rather than returningOk. A check that could not run must not report success — a caller cannot distinguish "validated" from "skipped".not declared, and emits no7bsize tag from a count it could not compute. A tag is quotable metadata that outlives the card.is_gated_ffnreturnsOption<bool>.ffn_hidden_sizehad two arms returning the same expression, so the gated branch was dead and its comment described behaviour the code did not have. Removed rather than "fixed" — doubling the value would be MLMF deciding what a gated FFN's hidden size is, which is a consumer's call.model_config_literals.rsscansModelConfigconstructions for invented literals. Every value it inspects changed shape in one commit:rope_theta: 10000.0becamerope_theta: Some(10000.0). ASome(..)reads as a call, calls are expressions, and that scanner permits expressions — so the change that removed the defect would also have made the defect invisible. Its own doc had named the hazard: "every form this fails to recognise is a SILENT PASS." It arrived by a route nobody anticipated, because the FIX rewrote the syntax rather than a person.The only thing that surfaced it was the file's non-vacuity assert firing.
Control — same sabotage, same tree, one variable:
is_literalrope_theta: Some(10000.0)Some(..)unwrapgguf.rs:703literal_fields > 0, which #48 made unsatisfiable. The old message advised deleting the guard as having no population. That would have been wrong. The class is not dead, it MOVED:onnx_import.rsstill seeds GPT-2's50257 / 768 / 12inlet mutinitialisers, one syntactic step outside what the scanner reads. The assert now counts model fields parsed rather than literals found — zero iff the parser is broken, positive on a clean tree — with the literal half proven by this file's own unit tests, which cannot be silenced by the tree changing shape.ONNX
max_position_embeddingswas seeded to 2048 and never assigned anywhere, so every ONNX model MLMF has ever loaded reported a 2048 context length unconditionally — not even as a fallback. NowNone.intermediate_size(GPT-2's 3072) likewise.vocab_size/hidden_size/num_hidden_layersare supplied by HF and GGUF (28/28 measured), so they are correctly concrete; ONNX needs a refusal, not anOption. Burying a behaviour change inside a type change is how it ships unreviewed.num_attention_headsis deferred with it — it is derived by division, andhead_dim()has 11 read sites and needs its own answer.CI does not test or lint the root
mlmfpackage, and that is deliberate and documented inci.yml(it needsprotoc, and spec §11 schedules it for rewrite). Control:cargo (test|clippy|build) -p mlmf-appears 68 times in that workflow; no step names the root package or--workspace. Every change in this PR exceptmodel_config_literals.rslives there, so a green CI tick is not evidence about it. The local runs below are the evidence.Taken at
origin/main93e7401:config.rshas 303 added lines and 4 warnings (279, 421, 229, 488) — both populations present, zero overlap.rope_thetafallback reddens both new tests; so does an independent one onintermediate_size.llama.rope.freq_base, got 100000from a real checkpoint — where the removed default was 10000, the factor of ten fix(gguf): read the model config from the file instead of fabricating it #37 measured.The two new tests, and why there are two
a_declared_value_and_a_silent_file_are_distinguishabledeclares exactly the constant the fallback invents. Any other fixture value would differ today and pass straight over the live defect; declaring the fallback's own number makes the two files identical in the output, which is the §6 tellCLAUDE.mdnames — "a caller cannot distinguish 'the model uses 10000' from 'MLMF had nothing to say.'" It failed red onleft: 10000.0, right: 10000.0.an_absent_key_is_none_and_a_declared_one_keeps_its_valueis not redundant:assert_ne!alone would be satisfied by a conversion returningSome(9999.0)for the silent file. It names both sides exactly, and pins thatnum_key_value_headsdid not move.🤖 Generated with Claude Code
https://claude.ai/code/session_01MdVuiraXRfDHQ227cjBt51
Summary by Sourcery
Represent unsupported or undisclosed model configuration fields as optional values and make downstream consumers refuse or clearly report unknown results instead of inventing model metadata.
Bug Fixes:
Noneinstead of fabricating architecture-specific or format-independent values.num_key_value_headsabsence behavior while correcting GGUF and HuggingFace field handling.Enhancements:
Tests: