feat(smollm3): add native SmolLM3 model family - #1158
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
SmolLM3-3B (model_type smollm3, SmolLM3ForCausalLM) is a dense decoder that matches the existing dense text contract except for one architectural difference: it interleaves NoPE layers among RoPE layers, marked by no_rope_layers (1 = the layer applies RoPE) or derived from no_rope_layer_interval. Resolve that schedule in the family config and consult it at each per-layer RoPE application site, so NoPE layers reach attention unrotated. The cos/sin tables stay global; only the per-layer application is gated. Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
SmolLM3's chat template is ChatML-framed but always emits a system block - Metadata (knowledge cutoff, current date, reasoning mode) plus mode-specific Custom Instructions - even when the caller supplies no system message. The generic ChatML path emitted none of it, and closed the no-think prefill with two newlines after </think> where upstream emits one. Add a smollm3 format that reproduces the upstream output byte for byte, detect it ahead of the generic ChatML fallback, and take the date as an injectable parameter so the contract is testable. Covered by three new cases in the model-owned chat-template test. Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
Verified against apply_chat_template() for HuggingFaceTB/SmolLM3-3B at the pinned revision: the system block runs straight into the user turn, so the earlier <|im_end|> after the instructions was wrong. Both reasoning modes now reproduce upstream byte for byte, and the tests assert the captured rendering (297 and 1369 bytes) rather than a reading of the Jinja source. Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
The family root was seeded from llama, so it carried contracts for checkpoints SmolLM3 does not ship: Llama-3.1 RoPE scaling (SmolLM3 declares rope_scaling null), and a Minitron chunked-prefill regression whose manifest is not part of this family. Those tests referenced manifests that do not exist here and failed on collection. Drop them, along with the two contract plugins and the invariant_only reference backend that only the removed cases exercised, so every plugin left is reachable from a declared manifest - the arrangement the other families keep. Rewrite the build contract against the SmolLM3-3B manifest: pinned revision, bf16 for the native KV path, exclusive GPU, the hf_transformers oracle, and no reliance on trust_remote_code. Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
…128k SmolLM3-3B ships rope_scaling null and a 65536-token window; the model card reaches 128k by raising max_position_embeddings to 131072 and adding a YaRN block. The family rejected that outright, so those checkpoints did not build. Extend the family's existing RoPE table with a yarn branch rather than adding a parallel path, so both decoder builders pick it up unchanged. Upstream folds attention_factor (0.1 * ln(factor) + 1) into cos/sin inside the rotary embedding instead of applying it to attention scores; omitting it leaves every entry ~6.9% short at factor=2.0, so it is applied to the table here and asserted separately. The native KV graph still declines scaled RoPE other than llama3, matching the other native-KV family, and now says so accurately: such checkpoints route to the standard decoder rather than failing. Verified against transformers' SmolLM3RotaryEmbedding: at positions 0-7 and at 70000-70007 the table sits 5.9e-08 from an exact float64 reference, closer than Hugging Face's own float32 result (3.2e-07 and 4.1e-03). Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
591bccd to
9f6b08c
Compare
…racts
test_model_plugin_encapsulation_static.py derives a family's C++ prefix as
"".join(part.capitalize() for part in model.split("_")), so model_type
"smollm3" requires Smollm3, not SmolLM3. Rename the runtime and test
identifiers. The model name stays as written wherever it is data rather than
an identifier: the chat-template system prompt and the comments quoting
upstream still read SmolLM3.
smollm3_detect_chat_template_format carried CCN 12 against the limit of 10.
The family format is the only one needing two markers, and that `&&` cost two
decision points on top of the ten the inherited chain already spent. An
ordered table of {first, second, require_both, format} rules scanned in
precedence order drops it to 7 and keeps smollm3-before-chatml explicit.
clang-format then reflows the renamed lines and splits the two custom
instruction constants into adjacent literals. The rendered prompts are
unchanged; test_smollm3_chat_template.cpp still asserts both reasoning modes
byte for byte against the captured upstream output.
The three dropped imports in the E2E contract module lost their only consumer
when the chunked-prefill regression plugin was removed.
Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
collectModelSupportInventory() resolves every E2E manifest's hf_id against website/data/hf-model-metadata.json and throws when an entry is missing, so the Docs stage failed on this family's manifest before the site could build. Add the checkpoint with the revision the manifest pins. The inventory also asserts that the two revisions agree, and that a non-empty architectures list carries both metadata_file and architecture_source, so model_type and architectures are taken from config.json at a07cc9a04f16550a088caea529712d1d335b0ac1 rather than the model card. revision_source is declared, matching the other manifest-pinned checkpoints. Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
A manifest with status ready is bound by two catalogs the family did not yet appear in, so the CPU unit stage failed on repository contracts rather than on anything under the family roots. trtmc_validate audits the validation workload catalog against every ready manifest and rejects an unlisted model. smollm3-3b declares user_contract continuation_parity, which is what mmlu_continuation_parity evaluates: a base completion model whose bundle and HF reference continue the same prompt greedily, compared token-level. The five-shot MMLU suite is the multiple_choice_qa contract and does not apply here. The release performance suite requires every non-L0 ready profile to be either a case or an explicitly excused profile. This change qualifies the family functionally and against the reference, but adds no performance workload or receipt, so the profile is excused for that reason, matching how the other dense family without a release-performance workload is recorded. Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
|
Hi @ruiling-smartbear — thanks for the thorough implementation. I pulled your Repro Every failure shares the same traceback: Root cause
Suggested fix Resolve the schedule from Want me to push a small PR against |
… contract test_every_fixed_kv_owner_uses_shared_explicit_attention scans every family module for add_kv_cache_update and asserts the discovered set equals a hardcoded roster, so a family that builds a fixed-capacity native KV graph fails the contract until it is listed. This family's graph_ops.py carries that call and was therefore in the discovered set but not the roster. List it. The two properties the contract exists to enforce already hold: graph_ops.py routes attention through add_explicit_masked_grouped_query_attention and never calls add_attention_v2, with the same occurrence counts as the qwen and llama modules it now sits beside. The companion contract that no family assigns IAttention.key_value_lengths was already satisfied. Signed-off-by: Ruilin Gao <ruiling@andrew.cmu.edu>
|
Hi @ruiling-smartbear — one small attribution request on the fix I posted. If you'd like to fold the rope_layer_schedule fix into your branch, please merge it in a way that keeps the commit's author (Moviw xvzimo@gmail.com) or adds a Co-authored-by: Moviw xvzimo@gmail.com trailer — happy to help you get to a real TensorRT box to finish the engine build + parity run, too. Thanks again for the family work. |
Background
HuggingFaceTB/SmolLM3-3Bis an Apache-2.0 dense decoder that no existing family resolves. Its published configuration declaresmodel_type: "smollm3"andSmolLM3ForCausalLM, so it is not an alias of a Llama or Qwen checkpoint, and one architectural difference keeps it off the dense Llama path: SmolLM3 interleaves NoPE layers among its RoPE layers, published asno_rope_layers(a1marks a layer that applies RoPE) or derived fromno_rope_layer_interval. For SmolLM3-3B that is layers 3, 7, 11, ... 35 — nine of thirty-six carry no positional encoding at all. Every other dense-contract requirement is already met: GQA 16/4, head_dim 128, SiLU, RMSNorm, no attention or MLP bias, tied embeddings,layer_typesuniformlyfull_attention.Opened as a draft: the implementation is complete and locally validated, but the engine build and the parity run still need a TensorRT box. Marking it ready once those are in.
Closes #1146.
Exit Criteria
trtmc build HuggingFaceTB/SmolLM3-3Bproduces a family-owned bundle withouttrust_remote_code, passing continuation parity against thehf_transformersreference at the declared thresholds.factor: 2.0,original_max_position_embeddings: 65536) builds.Implementation
families/smollm3/.ModelConfig.rope_layer_schedule()resolves the per-layer NoPE schedule fromno_rope_layers, falling back tono_rope_layer_intervaland finally to all-RoPE. Both decoder builders consult it at their RoPE application site, so NoPE layers reach attention unrotated; the cos/sin tables stay global and are simply unused there. A malformed schedule is rejected during build routing rather than surfacing deep in the graph builder.yarnbranch rather than a parallel path, so both builders pick it up unchanged. Upstream foldsattention_factor(0.1 * ln(factor) + 1) into cos/sin inside the rotary embedding instead of applying it to attention scores; omitting it leaves every entry ~6.9% short atfactor: 2.0, so it is applied to the table and asserted separately.src/runtime/models/smollm3/with strategy keysmollm3_decoder_kv_cache. SmolLM3's chat template is ChatML-framed but always emits a system block — metadata plus mode-specific custom instructions — even with no system message, and does not close that block with<|im_end|>. It is implemented as its own format, matched ahead of the generic ChatML fallback, with the date injectable so the contract is testable.tests/e2e/models/smollm3/with one manifest pinned to revisiona07cc9a04f16550a088caea529712d1d335b0ac1, bf16, exclusive GPU,hf_transformersoracle,causal_base_continuation/continuation_parity.llama3, matching the other native-KV family; such checkpoints route to the standard decoder rather than failing, and the routing reason now says so.kQwen3variant with digit group 3 and the\s+(?!\S)rule.Change categories
Validation
Rebased onto
61ac37f2; all results below were re-run on that base.Commands and Results
python3 tools/model_ci.py validate: passed, 93 registered model families,smollm3present, no descriptor errors.smollm3resolves tolibtrtmc_model_smollm3.so, pluginplugin.cpp|register_smollm3_plugin, strategysmollm3_decoder_kv_cacheunique repository-wide, all four declared runtime tests present.pytest tests/e2e/models/smollm3/: 58 passed, 13 skipped. The 6 remaining failures are the engine-building cases that require a real TensorRT module; the same six fail identically for thellamafamily in the same environment.pytest tests/e2e/models/smollm3/test_smollm3_rope_scaling.py: 7 passed — YaRN table against an exact float64 reference, the attention-factor fold, an explicitattention_factoroverride, three malformed-config rejections, and an unscaled-RoPE regression guard.pytest tests/e2e/models/smollm3/test_smollm3_build_contract.py: 6 passed — pinned revision, family runtime contract, bf16 for the native KV path, exclusive GPU,hf_transformersoracle, notrust_remote_code.pytest tests/e2e/models/smollm3/test_smollm3_family_plugin_weights.py: 3 passed, including tied embeddings (w_out == embedding.Twhenlm_head.weightis absent, which is the case for this checkpoint).Reference cross-checks
transformers, the builder's schedule equalsSmolLM3Attention.use_ropeon all 36 layers. Cross-checked behaviourally: driving one RoPE layer and one NoPE layer in isolation with identical hidden states but different relative spacing moves the RoPE layer by1.5e-03and the NoPE layer by exactly0. A global position shift is deliberately not used as the probe, since RoPE is a relative encoding and is invariant to it.5.9e-08away at positions 0-7 and5.9e-08at positions 70000-70007. Hugging Face's own float32 result is3.2e-07and4.1e-03at those positions, so the difference observed againstSmolLM3RotaryEmbeddingat long positions is upstream's float32 angle error, not this table's.tokenizer.json: Llama-3 style split regex,\p{N}{1,3},\s+(?!\S)present, ByteLevel withadd_prefix_space: false, BPE withignore_merges: true. The shared pre-tokenizer selectskQwen3and parses digit group 3, so no family-owned tokenizer work is required.apply_chat_template()at the pinned revision for both reasoning modes and asserted verbatim.lm_head.weight, no QK-norm, no biases. Running the family mapper over a checkpoint with that exact naming consumes every tensor and produces the tied output projection.Hardware, Environment, and Revisions
HuggingFaceTB/SmolLM3-3Bata07cc9a04f16550a088caea529712d1d335b0ac1.61ac37f2.Not Run / Remaining Gaps
tools.ci model-proofall still need a TensorRT box. This is why the PR is a draft.-Basecheckpoint are untested; v1 is scoped to one pinned contract case.Notes For Future Readers
config.py::rope_layer_schedule()first, then its three call sites; that single boolean is the whole architectural delta from the dense Llama contract.attention_factorfold is easy to drop when copying a RoPE table from another family.test_attention_factor_is_folded_into_the_tableexists to catch exactly that.Risk level
Risk rationale: a new self-contained family. No public API, ABI, bundle format, or dependency change. Every changed file sits under the four
smollm3roots — the Python family, the runtime DSO, the E2E root, and the C++ runtime tests — so nothing shared is touched and no existing family changes behavior.