Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

<br>
Expand Down
144 changes: 144 additions & 0 deletions docs/glm4_moe_lite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# GLM-4.7-Flash (GLM-4 MoE Lite)

<div class="kf-note kf-note--weights">
<b>Weights:</b> preconverted Keras weights are hosted at
<a href="https://huggingface.co/zeromodels/glm-4.7-flash">zeromodels/glm-4.7-flash</a>.
Load the model and tokenizer with
<code>from_weights("zeromodels/glm-4.7-flash")</code>. See
<a href="../loading_weights/">Loading Weights</a>.
</div>

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"]
```
82 changes: 82 additions & 0 deletions tests/integration/test_auto_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import inspect
from pathlib import Path

import pytest

Expand Down Expand Up @@ -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/<family>.md or an "
f"explicit _SHARED_MODEL_DOCS entry: {missing}"
)
1 change: 1 addition & 0 deletions website/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading