diff --git a/Makefile b/Makefile index 1f03db62..bd3366ef 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,6 @@ help: @echo " make test-saving Model save/load" @echo " make test-data-format channels_first/last" @echo " make test-data-format-gpu channels_first on TF GPU" - @echo " make test-links Link validation (slow)" @echo " make test-gpu All GPU-only tests" @echo "" diff --git a/README.md b/README.md index ab2417ae..c8e19aef 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Documentation sources are also available in [`docs/`](docs/). | GLM-4 (GLM-4-9B) | [ChatGLM: A Family of Large Language Models from GLM-130B to GLM-4](https://arxiv.org/abs/2406.12793) | `transformers` | | GLM-4-0414 | [THUDM/GLM-4-9B-0414](https://huggingface.co/THUDM/GLM-4-9B-0414) | `transformers` | | GLM-4.5 / GLM-4.6 (MoE) | [GLM-4.5: Agentic, Reasoning, and Coding (ARC) Foundation Models](https://arxiv.org/abs/2508.06471) | `transformers` | + | GLM-4.7-Flash (MoE Lite) | [zai-org/GLM-4.7-Flash](https://huggingface.co/zai-org/GLM-4.7-Flash) | `transformers` | | GLM-5 / GLM-5.1 / GLM-5.2 (MoE) | [GLM-5 Technical Report](https://arxiv.org/abs/2602.15763) | `transformers` |
diff --git a/docs/glm4_moe_lite.md b/docs/glm4_moe_lite.md new file mode 100644 index 00000000..81603e3e --- /dev/null +++ b/docs/glm4_moe_lite.md @@ -0,0 +1,144 @@ +# GLM-4.7-Flash (GLM-4 MoE Lite) + +
+Weights: preconverted Keras weights are hosted at +zeromodels/glm-4.7-flash. +Load the model and tokenizer with +from_weights("zeromodels/glm-4.7-flash"). See +Loading Weights. +
+ +GLM-4.7-Flash is a compact Mixture-of-Experts text model implemented in pure +Keras 3. Its ZeroModels architecture name is `glm4_moe_lite`. The decoder uses +DeepSeek-V3-style Multi-head Latent Attention (MLA) and aux-loss-free +DeepSeekMoE routing. Unlike GLM-5, it has no DSA sparse-attention indexer. + +All experts remain resident in memory even though only four routed experts are +active for each token. + +Links: + +- Model card: [zai-org/GLM-4.7-Flash](https://huggingface.co/zai-org/GLM-4.7-Flash) +- Keras weights: [zeromodels/glm-4.7-flash](https://huggingface.co/zeromodels/glm-4.7-flash) +- Related architecture: [GLM-4.5 (GLM-4 MoE)](glm4_moe.md) +- Related architecture: [GLM-5 MoE](glm5_moe.md) + +## Variant + +GLM-4 MoE Lite currently has one hosted variant. + +| Variant | Hosted | Upstream | +|---|---|---| +| `glm-4.7-flash` | `zeromodels/glm-4.7-flash` | [`zai-org/GLM-4.7-Flash`](https://huggingface.co/zai-org/GLM-4.7-Flash) | + +## API + +The family exports these public classes: + +- `Glm4MoeLiteConfig` +- `Glm4MoeLiteModel` +- `Glm4MoeLiteTextGenerate` +- `Glm4MoeLiteTokenizer` + +### `Glm4MoeLiteModel` + +The decoder backbone returns +`{"last_hidden_state": (batch, sequence, embed_dim)}`. + +| Arg | Default | Meaning | +|---|---|---| +| `vocab_size` | `154880` | token vocabulary size | +| `embed_dim` | `2048` | model width | +| `num_layers` | `47` | decoder blocks | +| `num_heads` | `20` | query heads | +| `mlp_dim` | `10240` | dense-layer SwiGLU width | +| `moe_mlp_dim` | `1536` | per-expert SwiGLU width | +| `num_experts` | `64` | routed expert count | +| `num_experts_per_tok` | `4` | routed experts selected per token | +| `n_shared_experts` | `1` | shared expert count | +| `n_group` | `1` | expert routing group count | +| `topk_group` | `1` | routing groups retained per token | +| `norm_topk_prob` | `True` | renormalize selected routing weights | +| `routed_scaling_factor` | `1.8` | selected-expert output scale | +| `first_k_dense` | `1` | leading dense decoder layers | +| `q_lora_rank` | `768` | query projection bottleneck | +| `kv_lora_rank` | `512` | key/value projection bottleneck | +| `qk_nope_head_dim` | `192` | non-rotary query/key width per head | +| `qk_rope_head_dim` | `64` | rotary query/key width per head | +| `v_head_dim` | `256` | value width per head | +| `rope_theta` | `1000000.0` | rotary base frequency | +| `rope_scaling` | `None` | optional Hugging Face YaRN scaling dictionary | +| `norm_eps` | `1e-5` | RMSNorm epsilon | +| `max_position_embeddings` | `202752` | maximum configured context length | +| `tie_embeddings` | `False` | reuse token embeddings for the LM head | + +### `Glm4MoeLiteTextGenerate` + +`Glm4MoeLiteModel` plus the language-model head. It returns +`{"logits": (batch, sequence, vocab_size)}` and adds `.generate()`. + +```python +generate( + input_ids, + attention_mask=None, + max_new_tokens=None, + eos_token_id=None, + sampler=None, + seed=None, + **prefill_inputs, +) +``` + +When `eos_token_id` is omitted, the generation class recognizes the model's +three end markers: `154820`, `154827`, and `154829`. + +### `Glm4MoeLiteTokenizer` + +The BPE tokenizer uses the `tokenizers` backend and loads `tokenizer.json` from +the Hub repository unless a local file is provided. + +```python +Glm4MoeLiteTokenizer(hf_id=None, tokenizer_file=None) +``` + +Calling it with a string or list of strings returns token IDs. Decode one +sequence with `.decode(ids)` or a batch with `.batch_decode(ids)`. + +## End-to-end example + +```python +import os + +os.environ["KERAS_BACKEND"] = "torch" # or "jax" / "tensorflow" + +from zeromodels.models.glm4_moe_lite import ( + Glm4MoeLiteTextGenerate, + Glm4MoeLiteTokenizer, +) + +weights = "zeromodels/glm-4.7-flash" +model = Glm4MoeLiteTextGenerate.from_weights( + weights, + load_dtype="bfloat16", +) +tokenizer = Glm4MoeLiteTokenizer.from_weights(weights) + +inputs = tokenizer("Explain mixture-of-experts routing in one sentence.") +outputs = model.generate(**inputs, max_new_tokens=64) +print(tokenizer.decode(outputs[0])) +``` + +The upstream model is large despite its sparse activation. Make sure the +selected backend has enough host and accelerator memory before converting it. + +## Backbone example + +```python +from zeromodels.models.glm4_moe_lite import Glm4MoeLiteModel + +backbone = Glm4MoeLiteModel.from_weights( + "zeromodels/glm-4.7-flash", + load_dtype="bfloat16", +) +hidden = backbone(inputs)["last_hidden_state"] +``` diff --git a/tests/integration/test_auto_registry.py b/tests/integration/test_auto_registry.py index 63e3469f..0873cbf5 100644 --- a/tests/integration/test_auto_registry.py +++ b/tests/integration/test_auto_registry.py @@ -9,6 +9,7 @@ """ import inspect +from pathlib import Path import pytest @@ -418,3 +419,84 @@ def family(class_name): "would return a mismatched (model, config, processor) triple; fix the mapping " f"(or allowlist a genuinely-shared type with its exact family set): {bad}" ) + + +# Families intentionally covered by a shared or differently named documentation page. +# Keep this mapping explicit: any new package must get its own docs page or be added here +# with the page that documents it. +_SHARED_MODEL_DOCS = { + "classification_backbones.md": { + "cait", + "convmixer", + "convnext", + "convnextv2", + "deit", + "densenet", + "efficientformer", + "efficientnet", + "efficientnet_lite", + "efficientnetv2", + "flexivit", + "inception_next", + "inception_resnetv2", + "inceptionv3", + "inceptionv4", + "levit", + "maxvit", + "mit", + "mlp_mixer", + "mobilenetv2", + "mobilenetv3", + "mobilenetv4", + "nextvit", + "pit", + "poolformer", + "regnet", + "res2net", + "resmlp", + "resnet", + "resnetv2", + "resnext", + "senet", + "swin", + "swinv2", + "vgg", + "vit", + "xception", + }, + "deberta.md": {"deberta_v2", "deberta_v3"}, + "dinov2.md": {"dino_v2"}, + "dinov3.md": {"dino_v3"}, +} + + +def test_every_model_family_is_documented(): + """Every model package has its own page or an explicit shared-page entry.""" + root = Path(__file__).resolve().parents[2] + docs_dir = root / "docs" + model_dirs = { + path.name + for path in (root / "zeromodels" / "models").iterdir() + if path.is_dir() and not path.name.startswith("_") + } + own_pages = {path.stem for path in docs_dir.glob("*.md")} + + shared_families = set() + for page, families in _SHARED_MODEL_DOCS.items(): + assert (docs_dir / page).is_file(), ( + f"shared model docs page does not exist: {page}" + ) + overlap = shared_families & families + assert not overlap, ( + f"model families listed under multiple shared pages: {overlap}" + ) + shared_families.update(families) + + stale = sorted(shared_families - model_dirs) + assert not stale, f"shared model docs entries without a model package: {stale}" + + missing = sorted(model_dirs - own_pages - shared_families) + assert not missing, ( + "model family package(s) without documentation; add docs/.md or an " + f"explicit _SHARED_MODEL_DOCS entry: {missing}" + ) diff --git a/website/mkdocs.yml b/website/mkdocs.yml index 4803be20..70b1dc7d 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -145,6 +145,7 @@ nav: - GLM: glm.md - GLM-4: glm4.md - GLM-4 MoE: glm4_moe.md + - GLM-4 MoE Lite: glm4_moe_lite.md - GLM-5 MoE: glm5_moe.md - GPT: gpt.md - GPT-2: gpt2.md