fix(scraper): never size hybrid SSM models by the estimator; cap by declared size (#1063)

* fix(scraper): never size hybrid SSM models by the estimator; cap by declared size

#1057 gated the packed-count correction on the repo being quantized, to
stop the estimator overriding the exact 31.6B count of the BF16
Nemotron-3-Nano with 101.6B. The first weekly run on that code (#1062,
not merged) showed the gate was wrong in both directions:

- Quantized Nemotron-H repos still went through the estimator, which
  prices every hybrid Mamba layer as a MoE transformer layer:
  nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4 (1.2M downloads)
  17.8B -> 101.6B, and 23 siblings.
- Unquantized repos lost the rescue the old rule gave a placeholder
  safetensors total: ornith-ai/Ornith-1.0-35B (2M downloads) 34.1B ->
  0.0B, NousResearch/Hermes-4-14B -> 0.0B, unsloth/Qwen3-235B-A22B-GGUF
  235B -> 16.4B.

The rule is now about what the estimator can size, not about
quantization:
- Hybrid SSM architectures (nemotron_h, zamba*, jamba, bamba, mamba*,
  falcon_h1, granitemoehybrid, lfm2*, ..., or any config carrying
  hybrid_override_pattern / layers_block_type) are never estimated. An
  understated count falls back to the size the repo name declares.
- Everything else keeps the original 'estimate > 2x reported' rescue,
  capped at the name-declared size when the estimate exceeds it by 1.5x.
- A speculative-decoding draft head keeps its own count: it is named
  after its target model but is a fraction of its size.

Of the 52 entries that moved in #1062 and declare a size in their name,
the number within 0.6-1.5x of it: main 32, #1062 10, this change 52.

* fix(scraper): match hybrid SSM architectures by family

Review follow-up on #1063: exact model_type matching missed variants the
catalog already holds (nemotron_h_puzzle, hybrid_mamba_attn, rwkv7_native,
lfm2_vl, qwen3_mamba3, ...). Over-matching is the safe direction: it keeps
the reported count or the declared size instead of an estimate.

---------

Co-authored-by: Alex Jones <axjns@example.com>
This commit is contained in:
Alex Jones 2026-09-19 12:51:42 +01:00 • committed by GitHub
parent 44de3655e0
commit 2dd06281bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 168 additions and 25 deletions

View file

@ -1446,23 +1446,92 @@ def is_prequantized_repo(repo_id: str, config: dict | None) -> bool:
return _name_says_prequantized(repo_id)
# Architectures estimate_params_from_arch cannot size: it prices every layer
# as attention + (MoE) MLP, but these interleave Mamba/SSM or linear-attention
# layers. It reported 101.6B for the 31.6B Nemotron-3-Nano and 17.0B for the
# 8.1B Nemotron-H-8B.
_HYBRID_SSM_MODEL_TYPE = re.compile(
r"mamba|rwkv|zamba|jamba|bamba|hyena|nemotron_h|falcon_h1|granitemoehybrid"
r"|lfm2|plamo2|recurrent_gemma|hybrid|(?:^|[_-])ssm(?:$|[_-])"
)
# A speculative-decoding draft head is named after its target model but is a
# fraction of its size; the safetensors count is right and the name is not.
_DRAFT_HEAD_NAME = re.compile(
r"(?<![a-z])(dflash|dspark|eagle\d?|medusa|speculator|draft)(?![a-z])", re.IGNORECASE)
_DECLARED_SIZE = re.compile(r"(?<![A-Za-z0-9.])(\d+(?:\.\d+)?)([BT])(?![A-Za-z0-9])")
def name_declared_params(repo_id: str) -> int | None:
"""Total parameters the repo name declares ("Qwen3-235B-A22B" -> 235e9).
None when the name declares nothing usable: no size token, an "NxMB"
expert product (Mixtral-8x7B is 46.7B, not 7B or 56B), or a draft head.
Active counts ("A22B") and effective sizes ("E4B") are not matched.
"""
base = repo_id.split("/")[-1]
if _DRAFT_HEAD_NAME.search(base) or re.search(r"\d+x\d", base, re.IGNORECASE):
return None
# "1.58B" is BitNet's bit width, not a size.
found = [(float(v), u) for v, u in _DECLARED_SIZE.findall(base.upper()) if v != "1.58"]
billions = [v * 1e9 for v, u in found if u == "B"]
# Next to a B size, a T figure is training tokens (bitnet-b1.58-2B-4T).
# On its own it is the size (Qwen3.8-2.4T-A95B).
sizes = billions or [v * 1e12 for v, u in found if u == "T"]
return int(max(sizes)) if sizes else None
def _is_hybrid_ssm(config: dict | None) -> bool:
cfg = config or {}
for src in (cfg, cfg.get("text_config") or {}):
# Families, not exact names: the catalog already holds variants such
# as nemotron_h_puzzle, hybrid_mamba_attn, rwkv7_native and lfm2_vl.
# Over-matching is the safe direction, since it only means keeping the
# reported count or the declared size instead of an estimate.
if _HYBRID_SSM_MODEL_TYPE.search(str(src.get("model_type", "")).lower()):
return True
if "hybrid_override_pattern" in src or "layers_block_type" in src:
return True
return False
def correct_packed_param_count(repo_id: str, total_params: int,
config: dict | None) -> int:
"""Replace a packed element count with the architecture estimate.
"""Replace an understated safetensors count with a better figure.
safetensors.total counts tensor elements, so int4 weights packed into
int32 report ~1/6 of the real parameters (#1045). That only happens to
quantized repos: for a full-precision checkpoint the count is exact, and
the estimate must not override it. The estimate over-counts hybrid models
(it prices every Nemotron-H layer as a MoE transformer layer, 101.6B for
a 31.6B BF16 checkpoint), which the 2x margin alone does not catch.
int32 report ~1/6 of the real parameters (#1045), and some repos publish
a placeholder (ornith-ai/Ornith-1.0-35B reports ~0). The architecture
estimate rescues both when it is more than twice the reported count.
Two guards, because the estimate is not always right:
- hybrid SSM architectures are never estimated (see
_HYBRID_SSM_MODEL_TYPE); an understated count falls back to the size
the name declares instead.
- any replacement more than 1.5x the name-declared size is capped to it.
A draft head keeps its own count: its name describes another model.
"""
if not is_prequantized_repo(repo_id, config):
base = repo_id.split("/")[-1]
if _DRAFT_HEAD_NAME.search(base):
return total_params
declared = name_declared_params(repo_id)
if _is_hybrid_ssm(config):
# A quantized repo's count is packed, so it only ever understates:
# NVFP4 stores two weights per element (17.8B for a 30B model), which
# the 2x margin misses. FP8 counts are exact and stay within 1.25x.
margin = 1.25 if is_prequantized_repo(repo_id, config) else 2.0
if declared and declared > total_params * margin:
return declared
return total_params
arch_params = estimate_params_from_arch(config)
if arch_params and arch_params > total_params * 2:
return arch_params
return total_params
if not arch_params or arch_params <= total_params * 2:
return total_params
if declared and arch_params > declared * 1.5:
return declared
return arch_params
def revalidation_priority(model: dict) -> int | None:

View file

@ -16,6 +16,7 @@ from scrape_hf_models import ( # noqa: E402
REVALIDATION_COOLDOWN_DAYS,
correct_packed_param_count,
is_prequantized_repo,
name_declared_params,
RATE_LIMIT_MAX_RETRIES,
RATE_LIMIT_STATS,
detect_moe,
@ -446,29 +447,99 @@ _QWEN38_27B_TEXT = {
}
def test_packed_count_is_corrected_only_for_quantized_repos():
_NEMOTRON_H_30B = {
"model_type": "nemotron_h", "hidden_size": 2688, "num_hidden_layers": 52,
"vocab_size": 131072, "num_attention_heads": 32, "num_key_value_heads": 2,
"head_dim": 128, "n_routed_experts": 128, "num_experts_per_tok": 6,
"moe_intermediate_size": 1856,
}
def test_packed_count_is_rescued_by_the_architecture_estimate():
# #1045: int4 packed into int32 reports 7.8B for a 27B-class model.
quantized = {"text_config": _QWEN38_27B_TEXT,
"quantization_config": {"quant_method": "compressed-tensors"}}
fixed = correct_packed_param_count(
"TelperionAI/Qwen3.8-27B-INT4-AWQ-GPTQ", 7_839_289_360, quantized)
assert 20e9 < fixed < 32e9, fixed
# The name alone is enough when config.json does not declare it.
# Not only quantized repos: ornith-ai/Ornith-1.0-35B publishes a
# placeholder total, and gating the rescue on quantization zeroed it.
fixed = correct_packed_param_count(
"RedHatAI/Qwen3-32B-quantized.w4a16", 7_839_289_360,
{"text_config": _QWEN38_27B_TEXT})
assert fixed > 20e9
# A full-precision checkpoint's count is exact. The estimator prices every
# Nemotron-H layer as a MoE transformer layer and said 101.6B for this one.
nemotron_h = {"hidden_size": 2688, "num_hidden_layers": 52, "vocab_size": 131072,
"num_attention_heads": 32, "num_key_value_heads": 2, "head_dim": 128,
"n_routed_experts": 128, "num_experts_per_tok": 6,
"moe_intermediate_size": 1856}
assert estimate_params_from_arch(nemotron_h) > 2 * 31_577_937_344
"org/Placeholder-27B", 1_000, {"text_config": _QWEN38_27B_TEXT})
assert 20e9 < fixed < 32e9, fixed
# A sound count is left alone.
assert correct_packed_param_count(
"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", 31_577_937_344, nemotron_h
"org/Model-27B", 26_900_000_000, {"text_config": _QWEN38_27B_TEXT}
) == 26_900_000_000
def test_hybrid_ssm_models_are_never_sized_by_the_estimator():
# It prices every Nemotron-H layer as a MoE transformer layer.
assert estimate_params_from_arch(_NEMOTRON_H_30B) > 3 * 30e9
# Full precision: the safetensors count is exact and stays.
assert correct_packed_param_count(
"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", 31_577_937_344, _NEMOTRON_H_30B
) == 31_577_937_344
# Packed NVFP4 (17.8B reported, 1.2M downloads): the weekly run wrote
# 101.6B for this. The name is the best figure available.
assert correct_packed_param_count(
"nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4", 17_800_000_000,
_NEMOTRON_H_30B) == 30_000_000_000
# FP8 stores one element per weight, so its count is already right.
assert correct_packed_param_count(
"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8", 31_600_000_000,
_NEMOTRON_H_30B) == 31_600_000_000
assert correct_packed_param_count(
"lmstudio-community/NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-4bit", 4_900_000_000,
_NEMOTRON_H_30B) == 30_000_000_000
# A hybrid with no size in its name keeps what safetensors said.
assert correct_packed_param_count(
"org/mystery-hybrid-awq", 4_900_000_000, _NEMOTRON_H_30B) == 4_900_000_000
# Families rather than exact names: variants already in the catalog.
for variant in ("nemotron_h_puzzle", "hybrid_mamba_attn", "rwkv7_native",
"lfm2_vl", "qwen3_mamba3", "qu_ssm_moe", "bailing_hybrid"):
cfg = {**_NEMOTRON_H_30B, "model_type": variant}
assert correct_packed_param_count("org/Variant-30B-4bit", 4_900_000_000, cfg) \
== 30_000_000_000, variant
# Ordinary transformers still reach the estimator.
for plain in ("qwen3_5", "llama", "deepseek_v4", "glm_moe_dsa", "gpt_oss"):
cfg = {"text_config": {**_QWEN38_27B_TEXT, "model_type": plain}}
assert correct_packed_param_count("org/Plain-27B-AWQ", 7_800_000_000, cfg) > 20e9, plain
# Detected by layout fields too, not only model_type.
layout = {**_NEMOTRON_H_30B, "model_type": "new_thing",
"hybrid_override_pattern": "MEMEM*E"}
assert correct_packed_param_count(
"org/New-30B-4bit", 4_900_000_000, layout) == 30_000_000_000
def test_estimate_is_capped_by_the_size_the_name_declares():
# An estimate far above the declared size is the estimator's error.
inflated = {**_NEMOTRON_H_30B, "model_type": "some_moe"}
assert correct_packed_param_count(
"org/Thing-30B-A3B-AWQ", 5_000_000_000, inflated) == 30_000_000_000
# A draft head is named after its target but is a fraction of its size.
assert correct_packed_param_count(
"z-lab/Qwen3.6-35B-A3B-DFlash", 400_000_000,
{"text_config": _QWEN38_27B_TEXT}) == 400_000_000
def test_name_declared_params():
cases = {
"Qwen/Qwen3-235B-A22B": 235e9,
"nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4": 30e9,
"Qwen/Qwen3.8-2.4T-A95B": 2.4e12,
"meta-llama/Llama-3.1-8B-Instruct": 8e9,
"ornith-ai/Ornith-1.0-35B": 35e9,
"microsoft/bitnet-b1.58-2B-4T": 2e9, # 4T is training tokens
"tzervas/qwen2.5-coder-32b-bitnet-1.58b": 32e9,
"mistralai/Mixtral-8x7B-Instruct-v0.1": None, # 46.7B, not 7B
"google/gemma-4-E4B-it": None, # effective size
"z-lab/Qwen3.6-35B-A3B-DFlash": None, # draft head
"deepseek-ai/DeepSeek-V4-Flash-0731": None,
}
for name, expected in cases.items():
got = name_declared_params(name)
assert got == (int(expected) if expected else None), (name, got)
def test_revalidation_keeps_the_retained_entry_on_a_sharp_parameter_drop():
@ -628,7 +699,10 @@ if __name__ == "__main__":
test_revalidation_ranks_uncorrectable_packed_counts_first,
test_revalidation_flags_suspect_context_and_missing_date,
test_revalidation_selection_respects_budget_and_skips_fresh_entries,
test_packed_count_is_corrected_only_for_quantized_repos,
test_packed_count_is_rescued_by_the_architecture_estimate,
test_hybrid_ssm_models_are_never_sized_by_the_estimator,
test_estimate_is_capped_by_the_size_the_name_declares,
test_name_declared_params,
test_revalidation_keeps_the_retained_entry_on_a_sharp_parameter_drop,
test_revalidation_cooldown_stops_failures_holding_the_budget,
test_unquantized_in_the_name_is_not_prequantized,