ydy9038074 commited on
Commit
a066584
·
verified ·
1 Parent(s): 3d50871

Publish Modilify Mk1 MLX runtime

Browse files
README.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: modilify-open-model-license-1.0
4
+ license_link: LICENSE
5
+ library_name: mlx
6
+ pipeline_tag: image-text-to-text
7
+ base_model: modilify/Modilify-Mk1
8
+ tags:
9
+ - mlx
10
+ - diffusion
11
+ - multimodal
12
+ - image-text-to-text
13
+ - mixture-of-experts
14
+ ---
15
+
16
+ ![LOGO](assets/01-LOGO.jpg)
17
+
18
+ # Modilify Mk1 MLX
19
+
20
+ Native MLX runtime for [Modilify Mk1](https://huggingface.co/modilify/Modilify-Mk1).
21
+
22
+ `model_type` is `modilify_mk1`. This package does not rewrite the checkpoint as another architecture and does not install into oMLX.
23
+
24
+ The published `model-*.safetensors` shards are a server-side copy of the official Mk1 weights. `modilify_mlx.load` remaps PyTorch parameter names at load time, so you do not need a second converted 49GB checkpoint on the Hub.
25
+
26
+ ## Layout
27
+
28
+ - `modilify_mlx/` — config, latent deliberation, exclusive commit policy, trunk wrapper, generate loop
29
+ - `convert_modilify_mk1_mlx.py` — official safetensors → MLX shards
30
+ - `generate_modilify.py` — standalone text trial inference
31
+
32
+ The heavy decoder / vision trunk is composed from mlx-vlm's DiffusionGemma layers as an internal dependency. Router top-k and the RMS-capped latent merge are Mk1's own forwards.
33
+
34
+ ## Convert
35
+
36
+ Use the oMLX bundled interpreter only as a Python that already has `mlx`:
37
+
38
+ ```bash
39
+ export PYTHONHOME=/Applications/oMLX.app/Contents/Resources/Python/cpython-3.11
40
+ export PYTHONPATH=/Applications/oMLX.app/Contents/Resources:/Applications/oMLX.app/Contents/Resources/Python/framework-mlx-base/lib/python3.11/site-packages
41
+ /Applications/oMLX.app/Contents/Resources/Python/cpython-3.11/bin/python3 \
42
+ ~/Modilify-Mk1-MLX/convert_modilify_mk1_mlx.py \
43
+ --source ~/Modilify-Mk1 \
44
+ --destination ~/Modilify-Mk1-MLX
45
+ ```
46
+
47
+ ## Generate
48
+
49
+ Official Mk1 shards and locally remapped MLX shards both load. Parameter names are remapped in `modilify_mlx.load`.
50
+
51
+ ```bash
52
+ /Applications/oMLX.app/Contents/Resources/Python/cpython-3.11/bin/python3 \
53
+ ~/Modilify-Mk1-MLX/generate_modilify.py \
54
+ --model ~/Modilify-Mk1-MLX \
55
+ --prompt "Explain why the sky is blue." \
56
+ --max-new-tokens 128
57
+ ```
58
+
59
+ Default is bf16 (`--expert-bits 16`). Add `--profile` to print per-phase denoise timings. Full-graph `mx.compile` of the decoder is blocked by MoE gather/router slices.
config.json ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "ModilifyMk1ForBlockDiffusion"
4
+ ],
5
+ "boi_token_id": 255999,
6
+ "bos_token_id": 2,
7
+ "canvas_length": 256,
8
+ "commit_failure_budget": 0.2,
9
+ "denoise_temperature": 0.8,
10
+ "dtype": "bfloat16",
11
+ "eoi_token_id": 258882,
12
+ "eos_token_id": [
13
+ 1,
14
+ 106
15
+ ],
16
+ "image_token_id": 258880,
17
+ "initializer_range": 0.02,
18
+ "jump_failure_budget": 2.0,
19
+ "jump_on_no_progress_after": 12,
20
+ "latent_dim": 1536,
21
+ "latent_dropout": 0.0,
22
+ "latent_local_attention_window": 128,
23
+ "latent_memory_slots": 64,
24
+ "latent_num_heads": 16,
25
+ "latent_num_layers": 4,
26
+ "max_ponder_steps": 64,
27
+ "min_trajectory_progress": 0.005,
28
+ "model_type": "modilify_mk1",
29
+ "pad_token_id": 0,
30
+ "text_config": {
31
+ "attention_bias": false,
32
+ "attention_dropout": 0.0,
33
+ "bos_token_id": 2,
34
+ "dtype": "bfloat16",
35
+ "eos_token_id": 1,
36
+ "final_logit_softcapping": 30.0,
37
+ "global_head_dim": 512,
38
+ "head_dim": 256,
39
+ "hidden_activation": "gelu_pytorch_tanh",
40
+ "hidden_size": 2816,
41
+ "initializer_range": 0.02,
42
+ "intermediate_size": 2112,
43
+ "layer_types": [
44
+ "sliding_attention",
45
+ "sliding_attention",
46
+ "sliding_attention",
47
+ "sliding_attention",
48
+ "sliding_attention",
49
+ "full_attention",
50
+ "sliding_attention",
51
+ "sliding_attention",
52
+ "sliding_attention",
53
+ "sliding_attention",
54
+ "sliding_attention",
55
+ "full_attention",
56
+ "sliding_attention",
57
+ "sliding_attention",
58
+ "sliding_attention",
59
+ "sliding_attention",
60
+ "sliding_attention",
61
+ "full_attention",
62
+ "sliding_attention",
63
+ "sliding_attention",
64
+ "sliding_attention",
65
+ "sliding_attention",
66
+ "sliding_attention",
67
+ "full_attention",
68
+ "sliding_attention",
69
+ "sliding_attention",
70
+ "sliding_attention",
71
+ "sliding_attention",
72
+ "sliding_attention",
73
+ "full_attention"
74
+ ],
75
+ "max_position_embeddings": 262144,
76
+ "model_type": "modilify_mk1_text",
77
+ "moe_intermediate_size": 704,
78
+ "num_attention_heads": 16,
79
+ "num_experts": 128,
80
+ "num_global_key_value_heads": 2,
81
+ "num_hidden_layers": 30,
82
+ "num_key_value_heads": 8,
83
+ "pad_token_id": 0,
84
+ "rms_norm_eps": 1e-06,
85
+ "rope_parameters": {
86
+ "full_attention": {
87
+ "partial_rotary_factor": 0.25,
88
+ "rope_theta": 1000000.0,
89
+ "rope_type": "proportional"
90
+ },
91
+ "sliding_attention": {
92
+ "rope_theta": 10000.0,
93
+ "rope_type": "default"
94
+ }
95
+ },
96
+ "sliding_window": 1024,
97
+ "tie_word_embeddings": true,
98
+ "top_k_experts": 8,
99
+ "use_bidirectional_attention": "vision",
100
+ "vocab_size": 262144
101
+ },
102
+ "tie_word_embeddings": true,
103
+ "transformers_version": "5.14.1",
104
+ "turn_end_token_id": 106,
105
+ "vision_config": {
106
+ "_name_or_path": "",
107
+ "architectures": null,
108
+ "attention_bias": false,
109
+ "attention_dropout": 0.0,
110
+ "chunk_size_feed_forward": 0,
111
+ "default_output_length": 280,
112
+ "dtype": "bfloat16",
113
+ "global_head_dim": 72,
114
+ "head_dim": 72,
115
+ "hidden_activation": "gelu_pytorch_tanh",
116
+ "hidden_size": 1152,
117
+ "id2label": {
118
+ "0": "LABEL_0",
119
+ "1": "LABEL_1"
120
+ },
121
+ "initializer_range": 0.02,
122
+ "intermediate_size": 4304,
123
+ "is_encoder_decoder": false,
124
+ "label2id": {
125
+ "LABEL_0": 0,
126
+ "LABEL_1": 1
127
+ },
128
+ "max_position_embeddings": 131072,
129
+ "model_type": "gemma4_vision",
130
+ "num_attention_heads": 16,
131
+ "num_hidden_layers": 27,
132
+ "num_key_value_heads": 16,
133
+ "output_attentions": false,
134
+ "output_hidden_states": false,
135
+ "patch_size": 16,
136
+ "pooling_kernel_size": 3,
137
+ "position_embedding_size": 10240,
138
+ "problem_type": null,
139
+ "return_dict": true,
140
+ "rms_norm_eps": 1e-06,
141
+ "rope_parameters": {
142
+ "rope_theta": 100.0,
143
+ "rope_type": "default"
144
+ },
145
+ "standardize": true,
146
+ "use_clipped_linears": false
147
+ },
148
+ "repetition_penalty": 1.0
149
+ }
convert_modilify_mk1_mlx.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2026 Modilify
3
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
4
+ """Convert an official Modilify Mk1 checkpoint into native MLX shards."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ import shutil
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import mlx.core as mx
15
+
16
+ ROOT = Path(__file__).resolve().parent
17
+ if str(ROOT) not in sys.path:
18
+ sys.path.insert(0, str(ROOT))
19
+
20
+ from modilify_mlx.config import MODEL_TYPE, TEXT_MODEL_TYPE, ModilifyMk1Config
21
+ from modilify_mlx.convert_utils import remap_weight, should_keep_source_key
22
+
23
+ COPY_PATTERNS = (
24
+ "tokenizer.json",
25
+ "tokenizer_config.json",
26
+ "chat_template.jinja",
27
+ "processor_config.json",
28
+ "generation_config.json",
29
+ "special_tokens_map.json",
30
+ "preprocessor_config.json",
31
+ )
32
+
33
+ MAX_SHARD_BYTES = 5 * 1024**3
34
+
35
+
36
+ def _load_json(path: Path) -> dict:
37
+ with path.open(encoding="utf-8") as handle:
38
+ return json.load(handle)
39
+
40
+
41
+ def _write_config(source: Path, destination: Path) -> None:
42
+ payload = _load_json(source / "config.json")
43
+ if payload.get("model_type") != MODEL_TYPE:
44
+ raise ValueError(
45
+ f"Source model_type must be {MODEL_TYPE!r}, got {payload.get('model_type')!r}."
46
+ )
47
+ payload.pop("auto_map", None)
48
+ payload["architectures"] = ["ModilifyMk1ForBlockDiffusion"]
49
+ text = dict(payload.get("text_config") or {})
50
+ text["model_type"] = TEXT_MODEL_TYPE
51
+ payload["text_config"] = text
52
+ payload["model_type"] = MODEL_TYPE
53
+ # Validate through the native config so a bad export fails here.
54
+ ModilifyMk1Config.from_dict(payload)
55
+ with (destination / "config.json").open("w", encoding="utf-8") as handle:
56
+ json.dump(payload, handle, indent=2)
57
+ handle.write("\n")
58
+
59
+
60
+ def _copy_sidecar_files(source: Path, destination: Path) -> None:
61
+ for name in COPY_PATTERNS:
62
+ src = source / name
63
+ if src.exists():
64
+ shutil.copy2(src, destination / name)
65
+
66
+
67
+ def _flush_shard(
68
+ destination: Path,
69
+ shard: dict[str, mx.array],
70
+ shard_index: int,
71
+ planned_count: int,
72
+ weight_map: dict[str, str],
73
+ ) -> int:
74
+ if not shard:
75
+ return shard_index
76
+ name = f"model-{shard_index:05d}-of-{planned_count:05d}.safetensors"
77
+ mx.save_safetensors(
78
+ str(destination / name),
79
+ shard,
80
+ metadata={"format": "mlx"},
81
+ )
82
+ for key in shard:
83
+ weight_map[key] = name
84
+ return shard_index + 1
85
+
86
+
87
+ def convert(source: Path, destination: Path) -> None:
88
+ source = source.resolve()
89
+ destination = destination.resolve()
90
+ destination.mkdir(parents=True, exist_ok=True)
91
+ index = _load_json(source / "model.safetensors.index.json")
92
+ source_map: dict[str, str] = index["weight_map"]
93
+ shard_names = []
94
+ for name in source_map.values():
95
+ if name not in shard_names:
96
+ shard_names.append(name)
97
+
98
+ remapped: dict[str, mx.array] = {}
99
+ kept_source = 0
100
+ dropped_source = 0
101
+ for shard_name in shard_names:
102
+ print(f"[convert] reading {shard_name}", flush=True)
103
+ loaded = mx.load(str(source / shard_name))
104
+ for key, value in loaded.items():
105
+ if not should_keep_source_key(key):
106
+ dropped_source += 1
107
+ continue
108
+ kept_source += 1
109
+ for new_key, new_value in remap_weight(key, value):
110
+ if new_key in remapped:
111
+ raise ValueError(f"Duplicate remapped key: {new_key}")
112
+ remapped[new_key] = new_value
113
+ del loaded
114
+
115
+ print(
116
+ f"[convert] kept {kept_source} source tensors, dropped {dropped_source}, "
117
+ f"wrote {len(remapped)} MLX tensors",
118
+ flush=True,
119
+ )
120
+ latent_keys = [key for key in remapped if key.startswith("latent_deliberation.")]
121
+ if len(latent_keys) < 150:
122
+ raise RuntimeError(
123
+ f"Expected the latent stack to survive conversion, found {len(latent_keys)} keys."
124
+ )
125
+
126
+ items = sorted(remapped.items())
127
+ shards: list[dict[str, mx.array]] = []
128
+ current: dict[str, mx.array] = {}
129
+ current_bytes = 0
130
+ for key, value in items:
131
+ tensor_bytes = int(value.nbytes)
132
+ if current and current_bytes + tensor_bytes > MAX_SHARD_BYTES:
133
+ shards.append(current)
134
+ current = {}
135
+ current_bytes = 0
136
+ current[key] = value
137
+ current_bytes += tensor_bytes
138
+ if current:
139
+ shards.append(current)
140
+
141
+ planned = max(1, len(shards))
142
+ weight_map: dict[str, str] = {}
143
+ total_size = 0
144
+ for index_i, shard in enumerate(shards, start=1):
145
+ print(f"[convert] writing shard {index_i}/{planned}", flush=True)
146
+ name = (
147
+ f"model-{index_i:05d}-of-{planned:05d}.safetensors"
148
+ if planned > 1
149
+ else "model.safetensors"
150
+ )
151
+ mx.save_safetensors(
152
+ str(destination / name),
153
+ shard,
154
+ metadata={"format": "mlx"},
155
+ )
156
+ for key, value in shard.items():
157
+ weight_map[key] = name
158
+ total_size += int(value.nbytes)
159
+
160
+ index_payload = {
161
+ "metadata": {"total_size": total_size},
162
+ "weight_map": {key: weight_map[key] for key in sorted(weight_map)},
163
+ }
164
+ with (destination / "model.safetensors.index.json").open(
165
+ "w", encoding="utf-8"
166
+ ) as handle:
167
+ json.dump(index_payload, handle, indent=2)
168
+ handle.write("\n")
169
+
170
+ _write_config(source, destination)
171
+ _copy_sidecar_files(source, destination)
172
+ print(f"[convert] done -> {destination}", flush=True)
173
+ print(f"[convert] tensors={len(weight_map)} bytes={total_size}", flush=True)
174
+
175
+
176
+ def main() -> None:
177
+ parser = argparse.ArgumentParser(description=__doc__)
178
+ parser.add_argument(
179
+ "--source",
180
+ type=Path,
181
+ default=Path.home() / "Modilify-Mk1",
182
+ help="Official Mk1 Hugging Face directory",
183
+ )
184
+ parser.add_argument(
185
+ "--destination",
186
+ type=Path,
187
+ default=ROOT,
188
+ help="Native MLX output directory",
189
+ )
190
+ args = parser.parse_args()
191
+ convert(args.source, args.destination)
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
generate_modilify.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2026 Modilify
3
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
4
+ """Standalone text trial-inference CLI for native Modilify Mk1 MLX."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import mlx.core as mx
13
+
14
+ ROOT = Path(__file__).resolve().parent
15
+ if str(ROOT) not in sys.path:
16
+ sys.path.insert(0, str(ROOT))
17
+
18
+ from modilify_mlx.generate import generate
19
+ from modilify_mlx.modeling import load
20
+
21
+
22
+ def _build_prompt_ids(model_path: Path, prompt: str, enable_thinking: bool):
23
+ from transformers import AutoTokenizer
24
+
25
+ tokenizer = AutoTokenizer.from_pretrained(str(model_path), trust_remote_code=True)
26
+ messages = [{"role": "user", "content": prompt}]
27
+ token_ids = tokenizer.apply_chat_template(
28
+ messages,
29
+ tokenize=True,
30
+ add_generation_prompt=True,
31
+ enable_thinking=enable_thinking,
32
+ )
33
+ if hasattr(token_ids, "input_ids"):
34
+ token_ids = token_ids.input_ids
35
+ elif isinstance(token_ids, dict):
36
+ token_ids = token_ids["input_ids"]
37
+ if hasattr(token_ids, "tolist"):
38
+ token_ids = token_ids.tolist()
39
+ if token_ids and isinstance(token_ids[0], (list, tuple)):
40
+ token_ids = token_ids[0]
41
+ return [int(token) for token in token_ids], tokenizer
42
+
43
+
44
+ def main() -> None:
45
+ parser = argparse.ArgumentParser(description=__doc__)
46
+ parser.add_argument("--model", type=Path, default=ROOT)
47
+ parser.add_argument("--prompt", default="Explain why the sky is blue.")
48
+ parser.add_argument("--max-new-tokens", type=int, default=128)
49
+ parser.add_argument("--temperature", type=float, default=None)
50
+ parser.add_argument("--enable-thinking", action="store_true")
51
+ parser.add_argument("--seed", type=int, default=0)
52
+ parser.add_argument(
53
+ "--expert-bits",
54
+ type=int,
55
+ default=16,
56
+ help="Quantize MoE experts to this bit width (16 keeps bf16).",
57
+ )
58
+ parser.add_argument(
59
+ "--profile",
60
+ action="store_true",
61
+ help="Print per-phase denoise timings.",
62
+ )
63
+ args = parser.parse_args()
64
+
65
+ print(f"[mk1] loading {args.model}", flush=True)
66
+ model, config = load(args.model, expert_bits=args.expert_bits)
67
+ if config.model_type != "modilify_mk1":
68
+ raise SystemExit(f"Refusing model_type={config.model_type!r}")
69
+ prompt_ids, tokenizer = _build_prompt_ids(
70
+ args.model, args.prompt, args.enable_thinking
71
+ )
72
+ print(
73
+ f"[mk1] prompt_tokens={len(prompt_ids)} canvas={config.canvas_length} "
74
+ f"temp={args.temperature if args.temperature is not None else config.denoise_temperature}",
75
+ flush=True,
76
+ )
77
+ profiler = None
78
+ if args.profile:
79
+ from modilify_mlx.profile import DenoiseProfiler
80
+
81
+ profiler = DenoiseProfiler()
82
+ output = generate(
83
+ model,
84
+ mx.array([prompt_ids], dtype=mx.int32),
85
+ max_new_tokens=args.max_new_tokens,
86
+ temperature=args.temperature,
87
+ seed=args.seed,
88
+ profiler=profiler,
89
+ )
90
+ text = tokenizer.decode(output.generated_ids, skip_special_tokens=False)
91
+ visible = tokenizer.decode(output.generated_ids, skip_special_tokens=True)
92
+ print(
93
+ f"[mk1] stop={output.stop_reason} denoise={output.denoise_steps} "
94
+ f"committed={output.generated_length} avg_commit={output.average_commit_len:.2f} "
95
+ f"tpf={output.tokens_per_forward:.2f} jumps={output.jump_count}",
96
+ flush=True,
97
+ )
98
+ print(
99
+ f"[mk1] prefill={output.prefill_seconds:.3f}s "
100
+ f"first_denoise={output.first_denoise_seconds:.3f}s "
101
+ f"generate={output.generate_seconds:.3f}s "
102
+ f"hd/s={output.heavy_denoise_per_second:.3f} "
103
+ f"steady_hd/s={output.steady_heavy_denoise_per_second:.3f} "
104
+ f"tok/s={output.tokens_per_second:.2f}",
105
+ flush=True,
106
+ )
107
+ if profiler is not None:
108
+ print(profiler.summary(), flush=True)
109
+ print("--- raw ---")
110
+ print(text)
111
+ print("--- visible ---")
112
+ print(visible)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
modilify_mlx/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Native MLX runtime for Modilify Mk1."""
4
+
5
+ from .config import ModilifyMk1Config
6
+ from .modeling import ModilifyMk1ForBlockDiffusion, load
7
+
8
+ __all__ = [
9
+ "ModilifyMk1Config",
10
+ "ModilifyMk1ForBlockDiffusion",
11
+ "load",
12
+ ]
modilify_mlx/commit_policy.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Confidence-and-entropy commit policy for Mk1 inference.
4
+
5
+ PyTorch → MLX:
6
+ fused_commit_confidence same excess-entropy sigmoid-square
7
+ prefix_failure_commit_lengths longest prefix with cumsum(risk) < budget
8
+ first_committed_token_lengths clip after first stop token
9
+ select_commit_lengths sampled prefix or greedy jump
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Sequence
15
+ from dataclasses import dataclass
16
+ import math
17
+
18
+ import mlx.core as mx
19
+
20
+ from .latent_deliberation import (
21
+ advance_trajectory_clocks,
22
+ should_force_trajectory_jump,
23
+ )
24
+
25
+ FUSED_EPS = 1e-6
26
+
27
+
28
+ def fused_commit_confidence(
29
+ proposal_confidence: mx.array,
30
+ token_entropy: mx.array,
31
+ *,
32
+ vocab_size: int = 256000,
33
+ eps: float = FUSED_EPS,
34
+ ) -> mx.array:
35
+ """Fuse proposal confidence with token entropy.
36
+
37
+ p = clamp(proposal_confidence, eps, 1 - eps)
38
+ h2 = -p * log(p) - (1 - p) * log(1 - p)
39
+ excess = max(token_entropy - h2, 0)
40
+ fused = sigmoid(logit(p) - excess) ** 2
41
+ """
42
+
43
+ del vocab_size
44
+ p = mx.clip(proposal_confidence.astype(mx.float32), eps, 1.0 - eps)
45
+ entropy = mx.maximum(token_entropy.astype(mx.float32), 0.0)
46
+ binary_entropy = -p * mx.log(p) - (1.0 - p) * mx.log1p(-p)
47
+ excess = mx.maximum(entropy - binary_entropy, 0.0)
48
+ logit_p = mx.log(p) - mx.log1p(-p)
49
+ fused = mx.square(mx.sigmoid(logit_p - excess))
50
+ return mx.clip(fused, eps, 1.0 - eps)
51
+
52
+
53
+ def fused_commit_failure_rate(
54
+ proposal_confidence: mx.array,
55
+ token_entropy: mx.array,
56
+ **kwargs: object,
57
+ ) -> mx.array:
58
+ """Return ``1 - fused_commit_confidence``."""
59
+
60
+ return 1.0 - fused_commit_confidence(
61
+ proposal_confidence, token_entropy, **kwargs
62
+ )
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class CommitPolicyDecision:
67
+ """One inference transition from proposal to committed prefix."""
68
+
69
+ normal_lengths: mx.array
70
+ commit_lengths: mx.array
71
+ commit_token_ids: mx.array
72
+ jump_rows: mx.array
73
+ ponder_steps: mx.array
74
+ stagnation_steps: mx.array
75
+
76
+
77
+ def prefix_failure_commit_lengths(
78
+ failure_rate: mx.array,
79
+ *,
80
+ failure_budget: float,
81
+ valid_mask: mx.array | None = None,
82
+ ) -> mx.array:
83
+ """Return the longest prefix with ``cumsum(failure_rate) < budget``."""
84
+
85
+ if failure_rate.ndim != 2:
86
+ raise ValueError("Failure rate must have shape [batch, canvas].")
87
+ if not math.isfinite(failure_budget) or failure_budget <= 0:
88
+ raise ValueError("Commit failure budget must be finite and positive.")
89
+ if valid_mask is None:
90
+ valid_mask = mx.ones(failure_rate.shape, dtype=mx.bool_)
91
+ if valid_mask.shape != failure_rate.shape:
92
+ raise ValueError("Commit validity mask must match failure rate.")
93
+
94
+ risk = mx.clip(failure_rate.astype(mx.float32), 0.0, 1.0) * valid_mask.astype(
95
+ mx.float32
96
+ )
97
+ cumulative_risk = mx.cumsum(risk, axis=-1)
98
+ contiguous_valid = mx.cumprod(valid_mask.astype(mx.int32), axis=-1).astype(
99
+ mx.bool_
100
+ )
101
+ allowed = (cumulative_risk < float(failure_budget)) & contiguous_valid
102
+ return mx.sum(mx.cumprod(allowed.astype(mx.int32), axis=-1), axis=-1)
103
+
104
+
105
+ def first_committed_token_lengths(
106
+ proposal: mx.array,
107
+ commit_lengths: mx.array,
108
+ token_id: int | Sequence[int],
109
+ ) -> mx.array:
110
+ """Clip each prefix immediately after its first stop token."""
111
+
112
+ if proposal.ndim != 2 or commit_lengths.shape != proposal.shape[:1]:
113
+ raise ValueError("Proposal and commit lengths must share a batch dimension.")
114
+ canvas = proposal.shape[1]
115
+ positions = mx.arange(canvas)[None, :]
116
+ committed = positions < commit_lengths[:, None]
117
+ if isinstance(token_id, int):
118
+ stop_token_ids = (int(token_id),)
119
+ else:
120
+ stop_token_ids = tuple(dict.fromkeys(int(value) for value in token_id))
121
+ if not stop_token_ids:
122
+ raise ValueError("At least one stop token ID is required.")
123
+ matches = proposal == stop_token_ids[0]
124
+ for value in stop_token_ids[1:]:
125
+ matches = matches | (proposal == value)
126
+ matches = matches & committed
127
+ sentinel = mx.full(positions.shape, canvas, dtype=positions.dtype)
128
+ first = mx.min(mx.where(matches, positions, sentinel), axis=-1)
129
+ clipped = mx.where(first < canvas, first + 1, commit_lengths)
130
+ return mx.minimum(clipped, commit_lengths)
131
+
132
+
133
+ def bounded_prefix_failure_commit_lengths(
134
+ committed_token_ids: mx.array,
135
+ failure_rate: mx.array,
136
+ *,
137
+ failure_budget: float,
138
+ remaining_lengths: mx.array,
139
+ stop_token_id: int | Sequence[int],
140
+ valid_mask: mx.array | None = None,
141
+ ) -> mx.array:
142
+ """Apply remaining-length and stop-token bounds to the prefix policy."""
143
+
144
+ if committed_token_ids.shape != failure_rate.shape:
145
+ raise ValueError(
146
+ "Committed token IDs and failure rate must share [batch, canvas]."
147
+ )
148
+ if remaining_lengths.shape != committed_token_ids.shape[:1]:
149
+ raise ValueError("Remaining lengths must have shape [batch].")
150
+ commit_lengths = prefix_failure_commit_lengths(
151
+ failure_rate,
152
+ failure_budget=failure_budget,
153
+ valid_mask=valid_mask,
154
+ )
155
+ commit_lengths = mx.minimum(commit_lengths, mx.maximum(remaining_lengths, 0))
156
+ return first_committed_token_lengths(
157
+ committed_token_ids,
158
+ commit_lengths,
159
+ stop_token_id,
160
+ )
161
+
162
+
163
+ def select_commit_lengths(
164
+ sampled_token_ids: mx.array,
165
+ normal_failure_rate: mx.array,
166
+ previous_failure_rate: mx.array,
167
+ greedy_token_ids: mx.array,
168
+ jump_failure_rate: mx.array,
169
+ *,
170
+ ponder_steps: mx.array,
171
+ stagnation_steps: mx.array,
172
+ active_rows: mx.array,
173
+ remaining_lengths: mx.array,
174
+ failure_budget: float,
175
+ jump_failure_budget: float,
176
+ stop_token_id: int | Sequence[int],
177
+ max_ponder_steps: int,
178
+ stagnation_threshold: int,
179
+ min_progress: float,
180
+ valid_mask: mx.array | None = None,
181
+ ) -> CommitPolicyDecision:
182
+ """Select sampled commits or a greedy jump after stagnation."""
183
+
184
+ if not (
185
+ sampled_token_ids.shape
186
+ == normal_failure_rate.shape
187
+ == previous_failure_rate.shape
188
+ == greedy_token_ids.shape
189
+ == jump_failure_rate.shape
190
+ ):
191
+ raise ValueError("Sampled and greedy statistics must share [batch, canvas].")
192
+
193
+ normal = bounded_prefix_failure_commit_lengths(
194
+ sampled_token_ids,
195
+ normal_failure_rate,
196
+ failure_budget=failure_budget,
197
+ remaining_lengths=remaining_lengths,
198
+ stop_token_id=stop_token_id,
199
+ valid_mask=valid_mask,
200
+ )
201
+ canvas_length = normal_failure_rate.shape[1]
202
+ previous_prefix_length = prefix_failure_commit_lengths(
203
+ previous_failure_rate,
204
+ failure_budget=failure_budget,
205
+ valid_mask=valid_mask,
206
+ )
207
+ frontier_length = mx.maximum(previous_prefix_length, normal) + 1
208
+ if valid_mask is not None:
209
+ valid_lengths = mx.sum(valid_mask.astype(mx.int32), axis=-1)
210
+ else:
211
+ valid_lengths = mx.full(frontier_length.shape, canvas_length)
212
+ frontier_length = mx.minimum(frontier_length, valid_lengths)
213
+ positions = mx.arange(canvas_length)[None, :]
214
+ progress_mask = positions < frontier_length[:, None]
215
+ if valid_mask is not None:
216
+ progress_mask = progress_mask & valid_mask
217
+ progress_mask = progress_mask & active_rows[:, None]
218
+ signed_improvement = previous_failure_rate.astype(mx.float32) - (
219
+ normal_failure_rate.astype(mx.float32)
220
+ )
221
+ weights = progress_mask.astype(mx.float32)
222
+ progress = mx.sum(signed_improvement * weights, axis=-1) / mx.maximum(
223
+ mx.sum(weights, axis=-1), 1.0
224
+ )
225
+ next_ponder, next_stagnation = advance_trajectory_clocks(
226
+ ponder_steps,
227
+ stagnation_steps,
228
+ commit_lengths=normal,
229
+ active_rows=active_rows,
230
+ progress_scores=progress,
231
+ min_progress=min_progress,
232
+ )
233
+ jump_rows = (
234
+ (normal == 0)
235
+ & active_rows
236
+ & should_force_trajectory_jump(
237
+ next_ponder,
238
+ next_stagnation,
239
+ max_ponder_steps=max_ponder_steps,
240
+ stagnation_threshold=stagnation_threshold,
241
+ )
242
+ )
243
+ jump_commit = bounded_prefix_failure_commit_lengths(
244
+ greedy_token_ids,
245
+ jump_failure_rate,
246
+ failure_budget=jump_failure_budget,
247
+ remaining_lengths=remaining_lengths,
248
+ stop_token_id=stop_token_id,
249
+ valid_mask=valid_mask,
250
+ )
251
+ committed = mx.where(jump_rows, jump_commit, normal)
252
+ commit_token_ids = mx.where(
253
+ jump_rows[:, None],
254
+ greedy_token_ids,
255
+ sampled_token_ids,
256
+ )
257
+ committed = first_committed_token_lengths(
258
+ commit_token_ids,
259
+ committed,
260
+ stop_token_id,
261
+ )
262
+ committed = mx.where(active_rows, committed, 0)
263
+ jump_rows = jump_rows & (committed > 0)
264
+ next_ponder = mx.where(committed > 0, 0, next_ponder).astype(mx.int32)
265
+ next_stagnation = mx.where(committed > 0, 0, next_stagnation).astype(mx.int32)
266
+ return CommitPolicyDecision(
267
+ normal_lengths=normal,
268
+ commit_lengths=committed,
269
+ commit_token_ids=commit_token_ids,
270
+ jump_rows=jump_rows,
271
+ ponder_steps=next_ponder,
272
+ stagnation_steps=next_stagnation,
273
+ )
modilify_mlx/config.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Serializable configuration for the native Modilify Mk1 MLX runtime."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from mlx_vlm.models.diffusion_gemma.config import ModelConfig, TextConfig
13
+ from mlx_vlm.models.gemma4.config import VisionConfig
14
+ from mlx_vlm.models.qwen3_vl.config import _config_kwargs, _maybe_deserialize_config
15
+
16
+ MODEL_TYPE = "modilify_mk1"
17
+ TEXT_MODEL_TYPE = "modilify_mk1_text"
18
+
19
+
20
+ def _require_model_type(payload: dict[str, Any], expected: str) -> None:
21
+ actual = payload.get("model_type")
22
+ if actual != expected:
23
+ raise ValueError(
24
+ f"Expected model_type {expected!r}, got {actual!r}. "
25
+ "This runtime only loads native Modilify Mk1 checkpoints."
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class ModilifyMk1Config:
31
+ """Mk1 inference configuration. ``model_type`` is always ``modilify_mk1``."""
32
+
33
+ text_config: TextConfig
34
+ vision_config: VisionConfig | None = None
35
+ model_type: str = MODEL_TYPE
36
+ boi_token_id: int = 255999
37
+ eoi_token_id: int = 258882
38
+ image_token_id: int = 258880
39
+ video_token_id: int | None = None
40
+ pad_token_id: int = 0
41
+ bos_token_id: int = 2
42
+ eos_token_id: list[int] | int = field(default_factory=lambda: [1, 106])
43
+ initializer_range: float = 0.02
44
+ canvas_length: int = 256
45
+ denoise_temperature: float = 0.8
46
+ commit_failure_budget: float = 0.2
47
+ jump_failure_budget: float = 2.0
48
+ jump_on_no_progress_after: int = 12
49
+ max_ponder_steps: int = 64
50
+ min_trajectory_progress: float = 0.005
51
+ latent_dim: int = 1536
52
+ latent_dropout: float = 0.0
53
+ latent_local_attention_window: int = 128
54
+ latent_memory_slots: int = 64
55
+ latent_num_heads: int = 16
56
+ latent_num_layers: int = 4
57
+ turn_end_token_id: int = 106
58
+ repetition_penalty: float = 1.0
59
+ dtype: str | None = "bfloat16"
60
+ generation_config: dict[str, Any] | None = None
61
+
62
+ def __post_init__(self) -> None:
63
+ if self.model_type != MODEL_TYPE:
64
+ raise ValueError(
65
+ f"ModilifyMk1Config.model_type must be {MODEL_TYPE!r}, "
66
+ f"got {self.model_type!r}."
67
+ )
68
+ if isinstance(self.text_config, dict):
69
+ text_payload = dict(self.text_config)
70
+ text_payload.setdefault("model_type", TEXT_MODEL_TYPE)
71
+ self.text_config = TextConfig(
72
+ **_config_kwargs(TextConfig, text_payload)
73
+ )
74
+
75
+ @classmethod
76
+ def from_dict(cls, payload: dict[str, Any]) -> "ModilifyMk1Config":
77
+ payload = dict(payload)
78
+ _require_model_type(payload, MODEL_TYPE)
79
+ text_payload = dict(payload.get("text_config") or {})
80
+ text_payload.setdefault("model_type", TEXT_MODEL_TYPE)
81
+ payload["text_config"] = TextConfig(
82
+ **_config_kwargs(TextConfig, text_payload)
83
+ )
84
+ if payload.get("vision_config") is not None:
85
+ payload["vision_config"] = _maybe_deserialize_config(
86
+ VisionConfig, payload.get("vision_config")
87
+ )
88
+ return cls(**_config_kwargs(cls, payload))
89
+
90
+ @classmethod
91
+ def from_json(cls, path: str | Path) -> "ModilifyMk1Config":
92
+ with Path(path).open(encoding="utf-8") as handle:
93
+ return cls.from_dict(json.load(handle))
94
+
95
+ def to_dict(self) -> dict[str, Any]:
96
+ def _as_dict(value: Any) -> Any:
97
+ if hasattr(value, "to_dict"):
98
+ return value.to_dict()
99
+ if hasattr(value, "__dict__") and not isinstance(
100
+ value, (str, int, float, bool, list, dict, type(None))
101
+ ):
102
+ return {
103
+ key: _as_dict(item)
104
+ for key, item in vars(value).items()
105
+ if not key.startswith("_")
106
+ }
107
+ return value
108
+
109
+ return {
110
+ "model_type": MODEL_TYPE,
111
+ "text_config": _as_dict(self.text_config),
112
+ "vision_config": None
113
+ if self.vision_config is None
114
+ else _as_dict(self.vision_config),
115
+ "boi_token_id": self.boi_token_id,
116
+ "eoi_token_id": self.eoi_token_id,
117
+ "image_token_id": self.image_token_id,
118
+ "video_token_id": self.video_token_id,
119
+ "pad_token_id": self.pad_token_id,
120
+ "bos_token_id": self.bos_token_id,
121
+ "eos_token_id": self.eos_token_id,
122
+ "initializer_range": self.initializer_range,
123
+ "canvas_length": self.canvas_length,
124
+ "denoise_temperature": self.denoise_temperature,
125
+ "commit_failure_budget": self.commit_failure_budget,
126
+ "jump_failure_budget": self.jump_failure_budget,
127
+ "jump_on_no_progress_after": self.jump_on_no_progress_after,
128
+ "max_ponder_steps": self.max_ponder_steps,
129
+ "min_trajectory_progress": self.min_trajectory_progress,
130
+ "latent_dim": self.latent_dim,
131
+ "latent_dropout": self.latent_dropout,
132
+ "latent_local_attention_window": self.latent_local_attention_window,
133
+ "latent_memory_slots": self.latent_memory_slots,
134
+ "latent_num_heads": self.latent_num_heads,
135
+ "latent_num_layers": self.latent_num_layers,
136
+ "turn_end_token_id": self.turn_end_token_id,
137
+ "repetition_penalty": self.repetition_penalty,
138
+ "dtype": self.dtype,
139
+ }
140
+
141
+ def trunk_model_config(self) -> ModelConfig:
142
+ """Build the borrowed trunk config object used only inside this package."""
143
+
144
+ return ModelConfig(
145
+ text_config=self.text_config,
146
+ vision_config=self.vision_config,
147
+ model_type=self.model_type,
148
+ boi_token_id=self.boi_token_id,
149
+ eoi_token_id=self.eoi_token_id,
150
+ image_token_id=self.image_token_id,
151
+ video_token_id=self.video_token_id,
152
+ initializer_range=self.initializer_range,
153
+ canvas_length=self.canvas_length,
154
+ eos_token_id=self.eos_token_id,
155
+ generation_config=self.generation_config,
156
+ dtype=self.dtype,
157
+ )
158
+
159
+ @property
160
+ def vocab_size(self) -> int:
161
+ return int(self.text_config.vocab_size)
162
+
163
+ @property
164
+ def hidden_size(self) -> int:
165
+ return int(self.text_config.hidden_size)
modilify_mlx/convert_utils.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Weight remapping from the official Mk1 safetensors export to MLX."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Iterable
8
+
9
+ import mlx.core as mx
10
+
11
+ # PyTorch → MLX
12
+ # *.experts.down_proj *.experts.down_proj.weight
13
+ # *.experts.gate_up_proj *.experts.gate_up_proj.weight
14
+ # *.in_proj_weight [3D, D] query/key/value_proj.weight
15
+ # *.in_proj_bias [3D] query/key/value_proj.bias
16
+ # token_ff.0 / token_ff.2 token_ff.layers.0 / token_ff.layers.2
17
+ # memory_ff.0 / memory_ff.2 memory_ff.layers.0 / memory_ff.layers.2
18
+
19
+ _ATTENTION_MODULES = (
20
+ "local_attention",
21
+ "token_memory_attention",
22
+ "memory_token_attention",
23
+ )
24
+
25
+ _SKIP_SUBSTRINGS = (
26
+ "rotary_emb",
27
+ "lm_head.weight",
28
+ )
29
+
30
+ _CLIP_MARKERS = ("input_max", "input_min", "output_max", "output_min")
31
+
32
+
33
+ def should_keep_source_key(key: str) -> bool:
34
+ if any(marker in key for marker in _SKIP_SUBSTRINGS):
35
+ return False
36
+ if key.startswith("model.encoder.language_model.") and not key.endswith(
37
+ ".layer_scalar"
38
+ ):
39
+ return False
40
+ if key.startswith("model.encoder.vision_tower.") or key.startswith(
41
+ "model.encoder.embed_vision."
42
+ ):
43
+ if any(marker in key for marker in _CLIP_MARKERS):
44
+ return False
45
+ return True
46
+
47
+
48
+ def _split_qkv(prefix: str, value: mx.array) -> list[tuple[str, mx.array]]:
49
+ if value.ndim == 1:
50
+ width = value.shape[0]
51
+ if width % 3:
52
+ raise ValueError(f"Cannot split QKV bias for {prefix}: shape {value.shape}")
53
+ head = width // 3
54
+ pieces = (value[:head], value[head : 2 * head], value[2 * head :])
55
+ names = ("query_proj.bias", "key_proj.bias", "value_proj.bias")
56
+ elif value.ndim == 2:
57
+ width = value.shape[0]
58
+ if width % 3:
59
+ raise ValueError(
60
+ f"Cannot split QKV weight for {prefix}: shape {value.shape}"
61
+ )
62
+ head = width // 3
63
+ pieces = (value[:head], value[head : 2 * head], value[2 * head :])
64
+ names = ("query_proj.weight", "key_proj.weight", "value_proj.weight")
65
+ if pieces[0].shape[0] != pieces[0].shape[1]:
66
+ raise ValueError(
67
+ f"Split QKV weight for {prefix} is not square: {pieces[0].shape}"
68
+ )
69
+ else:
70
+ raise ValueError(f"Unexpected QKV tensor rank for {prefix}: {value.shape}")
71
+ return [(f"{prefix}.{name}", piece) for name, piece in zip(names, pieces)]
72
+
73
+
74
+ def remap_weight(key: str, value: mx.array) -> list[tuple[str, mx.array]]:
75
+ """Map one official Mk1 tensor onto one or more MLX parameter names."""
76
+
77
+ if not should_keep_source_key(key):
78
+ return []
79
+
80
+ if key.endswith(".experts.down_proj"):
81
+ return [(key + ".weight", value)]
82
+ if key.endswith(".experts.gate_up_proj"):
83
+ return [(key + ".weight", value)]
84
+
85
+ for module in _ATTENTION_MODULES:
86
+ in_proj_weight = f".{module}.in_proj_weight"
87
+ in_proj_bias = f".{module}.in_proj_bias"
88
+ if key.endswith(in_proj_weight):
89
+ prefix = key[: -len(".in_proj_weight")]
90
+ return _split_qkv(prefix, value)
91
+ if key.endswith(in_proj_bias):
92
+ prefix = key[: -len(".in_proj_bias")]
93
+ return _split_qkv(prefix, value)
94
+
95
+ if ".token_ff.0." in key or key.endswith(".token_ff.0.weight") or key.endswith(
96
+ ".token_ff.0.bias"
97
+ ):
98
+ return [(key.replace(".token_ff.0.", ".token_ff.layers.0."), value)]
99
+ if ".token_ff.2." in key or key.endswith(".token_ff.2.weight") or key.endswith(
100
+ ".token_ff.2.bias"
101
+ ):
102
+ return [(key.replace(".token_ff.2.", ".token_ff.layers.2."), value)]
103
+ if ".memory_ff.0." in key or key.endswith(".memory_ff.0.weight") or key.endswith(
104
+ ".memory_ff.0.bias"
105
+ ):
106
+ return [(key.replace(".memory_ff.0.", ".memory_ff.layers.0."), value)]
107
+ if ".memory_ff.2." in key or key.endswith(".memory_ff.2.weight") or key.endswith(
108
+ ".memory_ff.2.bias"
109
+ ):
110
+ return [(key.replace(".memory_ff.2.", ".memory_ff.layers.2."), value)]
111
+
112
+ return [(key, value)]
113
+
114
+
115
+ def remap_state_dict(
116
+ source: Iterable[tuple[str, mx.array]],
117
+ ) -> dict[str, mx.array]:
118
+ remapped: dict[str, mx.array] = {}
119
+ for key, value in source:
120
+ for new_key, new_value in remap_weight(key, value):
121
+ if new_key in remapped:
122
+ raise ValueError(f"Duplicate remapped key: {new_key}")
123
+ remapped[new_key] = new_value
124
+ return remapped
modilify_mlx/fast_decode.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Compile-friendly Mk1 decoder: static prefix KV + fixed-shape sliding gather."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+ import time
9
+
10
+ import mlx.core as mx
11
+ import mlx.nn as nn
12
+
13
+ from mlx_vlm.models.base import scaled_dot_product_attention
14
+ from mlx_vlm.models.diffusion_gemma.language import geglu
15
+
16
+
17
+ def make_static_cache(encoder, max_size: int):
18
+ """Fixed-capacity prefix cache so decoder_state has a static key length."""
19
+
20
+ return encoder.make_cache(max_size=max(int(max_size), 1))
21
+
22
+
23
+ def _cache_capacity(cache) -> int:
24
+ first = cache[0]
25
+ keys = getattr(first, "keys", None)
26
+ if keys is None:
27
+ return int(getattr(first, "max_size", 0) or 0)
28
+ return int(keys.shape[2])
29
+
30
+
31
+ def _prefix_length(cache) -> int:
32
+ offset = getattr(cache[0], "offset", 0)
33
+ if isinstance(offset, mx.array):
34
+ return int(mx.max(offset).item())
35
+ return int(offset)
36
+
37
+
38
+ def build_decoder_masks(
39
+ *,
40
+ prefix_len: int,
41
+ canvas_length: int,
42
+ cache_capacity: int,
43
+ sliding_window: int,
44
+ batch_size: int = 1,
45
+ ) -> tuple[mx.array, mx.array]:
46
+ """Boolean SDPA masks with static shapes for full and sliding layers."""
47
+
48
+ cache_valid = mx.arange(cache_capacity) < int(prefix_len)
49
+ canvas_valid = mx.ones((canvas_length,), dtype=mx.bool_)
50
+ full_row = mx.concatenate([cache_valid, canvas_valid], axis=0)
51
+ full = mx.broadcast_to(
52
+ full_row.reshape(1, 1, 1, -1),
53
+ (batch_size, 1, canvas_length, cache_capacity + canvas_length),
54
+ )
55
+
56
+ window = max(int(sliding_window) - 1, 1)
57
+ slide_k = min(window, cache_capacity)
58
+ if slide_k < cache_capacity:
59
+ raw_idx = mx.arange(slide_k) + (int(prefix_len) - slide_k)
60
+ slide_valid = (raw_idx >= 0) & (raw_idx < int(prefix_len))
61
+ else:
62
+ slide_valid = mx.arange(cache_capacity) < int(prefix_len)
63
+ slide_row = mx.concatenate([slide_valid, canvas_valid], axis=0)
64
+ slide = mx.broadcast_to(
65
+ slide_row.reshape(1, 1, 1, -1),
66
+ (batch_size, 1, canvas_length, slide_k + canvas_length),
67
+ )
68
+ return full, slide
69
+
70
+
71
+ def _gather_window(tensor: mx.array, offset: mx.array, window: int) -> mx.array:
72
+ max_size = tensor.shape[2]
73
+ idx = mx.clip(mx.arange(window) + offset - window, 0, max_size - 1)
74
+ gather = idx.reshape(1, 1, window, 1)
75
+ return mx.take_along_axis(tensor, gather, axis=2)
76
+
77
+
78
+ def _decoder_attention(
79
+ attn: nn.Module,
80
+ x: mx.array,
81
+ mask: mx.array | None,
82
+ cache,
83
+ offset: mx.array,
84
+ ) -> mx.array:
85
+ batch, length, _ = x.shape
86
+ queries = attn.q_proj(x).reshape(batch, length, attn.n_heads, attn.head_dim)
87
+ queries = attn.q_norm(queries).transpose(0, 2, 1, 3)
88
+ queries = attn.rope(queries, offset=offset)
89
+
90
+ keys = attn.k_proj(x).reshape(batch, length, attn.n_kv_heads, attn.head_dim)
91
+ values = (
92
+ attn.v_proj(x).reshape(batch, length, attn.n_kv_heads, attn.head_dim)
93
+ if attn.v_proj is not None
94
+ else keys
95
+ )
96
+ keys = attn.k_norm(keys).transpose(0, 2, 1, 3)
97
+ keys = attn.rope(keys, offset=offset)
98
+ values = attn.v_norm(values).transpose(0, 2, 1, 3)
99
+
100
+ encoder_keys, encoder_values = cache.decoder_state
101
+ if attn.is_sliding:
102
+ window = max(int(attn.config.sliding_window) - 1, 1)
103
+ slide_k = min(window, int(encoder_keys.shape[2]))
104
+ if slide_k < int(encoder_keys.shape[2]):
105
+ encoder_keys = _gather_window(encoder_keys, offset, slide_k)
106
+ encoder_values = _gather_window(encoder_values, offset, slide_k)
107
+ keys = mx.concatenate([encoder_keys, keys], axis=2)
108
+ values = mx.concatenate([encoder_values, values], axis=2)
109
+ output = scaled_dot_product_attention(
110
+ queries, keys, values, cache=None, scale=attn.scale, mask=mask
111
+ )
112
+ output = output.transpose(0, 2, 1, 3).reshape(batch, length, -1)
113
+ return attn.o_proj(output)
114
+
115
+
116
+ def _experts_unsorted(experts: nn.Module, x: mx.array, top_k_indices, top_k_weights):
117
+ """Expert FFN without argsort gather, so the decoder graph can compile."""
118
+
119
+ x = mx.expand_dims(x, (-2, -3))
120
+ gate_up = experts.gate_up_proj(x, top_k_indices, sorted_indices=False)
121
+ gate = gate_up[..., : experts.hidden_dims]
122
+ up = gate_up[..., experts.hidden_dims :]
123
+ y = experts.down_proj(geglu(gate, up), top_k_indices, sorted_indices=False)
124
+ y = y.squeeze(-2)
125
+ return (y * top_k_weights[..., None]).sum(axis=-2)
126
+
127
+
128
+ def _decoder_layer(
129
+ layer: nn.Module,
130
+ x: mx.array,
131
+ mask: mx.array | None,
132
+ cache,
133
+ offset: mx.array,
134
+ ) -> mx.array:
135
+ residual = x
136
+ hidden = layer.input_layernorm(x)
137
+ hidden = _decoder_attention(layer.self_attn, hidden, mask, cache, offset)
138
+ hidden = layer.post_attention_layernorm(hidden)
139
+ hidden = residual + hidden
140
+
141
+ residual = hidden
142
+ shared = layer.pre_feedforward_layernorm(hidden)
143
+ shared = layer.mlp(shared)
144
+ shared = layer.post_feedforward_layernorm_1(shared)
145
+
146
+ flat = residual.reshape(-1, residual.shape[-1])
147
+ top_k_indices, top_k_weights = layer.router(flat)
148
+ routed = layer.pre_feedforward_layernorm_2(flat)
149
+ routed = layer.experts(routed, top_k_indices, top_k_weights)
150
+ routed = routed.reshape(residual.shape)
151
+ routed = layer.post_feedforward_layernorm_2(routed)
152
+
153
+ hidden = layer.post_feedforward_layernorm(shared + routed)
154
+ return residual + hidden
155
+
156
+
157
+ def _attn_residual(
158
+ layer: nn.Module,
159
+ x: mx.array,
160
+ mask: mx.array | None,
161
+ cache,
162
+ offset: mx.array,
163
+ ) -> mx.array:
164
+ residual = x
165
+ hidden = layer.input_layernorm(x)
166
+ hidden = _decoder_attention(layer.self_attn, hidden, mask, cache, offset)
167
+ hidden = layer.post_attention_layernorm(hidden)
168
+ return residual + hidden
169
+
170
+
171
+ def _ffn_residual(layer: nn.Module, hidden: mx.array) -> mx.array:
172
+ residual = hidden
173
+ shared = layer.pre_feedforward_layernorm(hidden)
174
+ shared = layer.mlp(shared)
175
+ shared = layer.post_feedforward_layernorm_1(shared)
176
+
177
+ flat = residual.reshape(-1, residual.shape[-1])
178
+ top_k_indices, top_k_weights = layer.router(flat)
179
+ routed = layer.pre_feedforward_layernorm_2(flat)
180
+ routed = layer.experts(routed, top_k_indices, top_k_weights)
181
+ routed = routed.reshape(residual.shape)
182
+ routed = layer.post_feedforward_layernorm_2(routed)
183
+
184
+ hidden = layer.post_feedforward_layernorm(shared + routed)
185
+ return (residual + hidden) * layer.layer_scalar
186
+
187
+
188
+ def make_compiled_attn_layers(decoder: nn.Module, cache) -> list:
189
+ """Compile attention residuals only. Expert FFNs stay eager."""
190
+
191
+ compiled = []
192
+ for layer, layer_cache in zip(decoder.layers, cache):
193
+ def _fn(x, offset, mask, _layer=layer, _cache=layer_cache):
194
+ return _attn_residual(_layer, x, mask, _cache, offset)
195
+
196
+ compiled.append(mx.compile(_fn, shapeless=True))
197
+ return compiled
198
+
199
+
200
+ def decoder_hidden_states(
201
+ decoder: nn.Module,
202
+ canvas_ids: mx.array,
203
+ latent_context: mx.array,
204
+ cache,
205
+ offset: mx.array,
206
+ full_mask: mx.array,
207
+ slide_mask: mx.array,
208
+ compiled_attn_layers=None,
209
+ profiler=None,
210
+ ) -> mx.array:
211
+ hidden = decoder._embed_canvas(
212
+ canvas_ids,
213
+ self_conditioning_embeddings=latent_context,
214
+ )
215
+ if profiler is None:
216
+ for layer, layer_cache in zip(decoder.layers, cache):
217
+ mask = slide_mask if layer.layer_type == "sliding_attention" else full_mask
218
+ hidden = _decoder_layer(layer, hidden, mask, layer_cache, offset)
219
+ hidden = hidden * layer.layer_scalar
220
+ return decoder.norm(hidden)
221
+
222
+ attn_acc = 0.0
223
+ moe_acc = 0.0
224
+ for layer, layer_cache in zip(decoder.layers, cache):
225
+ mask = slide_mask if layer.layer_type == "sliding_attention" else full_mask
226
+ mx.eval(hidden)
227
+ started = time.perf_counter()
228
+ hidden = _attn_residual(layer, hidden, mask, layer_cache, offset)
229
+ mx.eval(hidden)
230
+ attn_acc += time.perf_counter() - started
231
+ started = time.perf_counter()
232
+ hidden = _ffn_residual(layer, hidden)
233
+ mx.eval(hidden)
234
+ moe_acc += time.perf_counter() - started
235
+ profiler.add("attn", attn_acc)
236
+ profiler.add("moe", moe_acc)
237
+ return decoder.norm(hidden)
238
+
239
+
240
+ def make_compiled_decoder(
241
+ decoder: nn.Module,
242
+ cache,
243
+ softcap: float,
244
+ ):
245
+ """Compile canvas + latent-context → (logits, hidden)."""
246
+
247
+ def _step(
248
+ canvas_ids: mx.array,
249
+ latent_context: mx.array,
250
+ offset: mx.array,
251
+ full_mask: mx.array,
252
+ slide_mask: mx.array,
253
+ ):
254
+ hidden = decoder_hidden_states(
255
+ decoder,
256
+ canvas_ids,
257
+ latent_context,
258
+ cache,
259
+ offset,
260
+ full_mask,
261
+ slide_mask,
262
+ )
263
+ logits = decoder.embed_tokens.as_linear(hidden)
264
+ logits = mx.tanh(logits.astype(mx.float32) / softcap) * softcap
265
+ return logits, hidden
266
+
267
+ return mx.compile(_step, shapeless=True)
268
+
269
+
270
+ def quantize_experts(model: nn.Module, *, bits: int = 8, group_size: int = 64) -> None:
271
+ """Quantize MoE expert projections only. Attention / embeddings stay bf16."""
272
+
273
+ if bits >= 16:
274
+ return
275
+
276
+ def predicate(path: str, module: nn.Module):
277
+ if "experts" not in path:
278
+ return False
279
+ if not hasattr(module, "to_quantized"):
280
+ return False
281
+ if not (path.endswith("gate_up_proj") or path.endswith("down_proj")):
282
+ return False
283
+ return {"group_size": group_size, "bits": int(bits), "mode": "affine"}
284
+
285
+ print(f"[mk1] quantizing experts to {bits}-bit", flush=True)
286
+ before = sum(arr.nbytes for _, arr in model.parameters().items()) if False else None
287
+ del before
288
+ quantized = {"count": 0}
289
+
290
+ def counting_predicate(path: str, module: nn.Module):
291
+ result = predicate(path, module)
292
+ if result:
293
+ quantized["count"] += 1
294
+ return result
295
+
296
+ nn.quantize(model, class_predicate=counting_predicate)
297
+ print(f"[mk1] quantized {quantized['count']} expert projections", flush=True)
298
+ mx.eval(model.parameters())
modilify_mlx/generate.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Rolling canvas generation for native Modilify Mk1."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, replace
8
+ import math
9
+ import time
10
+ from typing import Any
11
+
12
+ import mlx.core as mx
13
+
14
+ from mlx_vlm.generate.common import wired_limit
15
+
16
+ from .commit_policy import fused_commit_failure_rate, select_commit_lengths
17
+ from .latent_deliberation import LatentDeliberationState
18
+ from .modeling import ModilifyMk1ForBlockDiffusion
19
+
20
+
21
+ @dataclass
22
+ class ModilifyMk1GenerationOutput:
23
+ sequences: list[int]
24
+ generated_ids: list[int]
25
+ generated_length: int
26
+ denoise_steps: int
27
+ jump_count: int
28
+ average_commit_len: float
29
+ stop_reason: str
30
+ tokens_per_forward: float
31
+ prefill_seconds: float = 0.0
32
+ generate_seconds: float = 0.0
33
+ first_denoise_seconds: float = 0.0
34
+ heavy_denoise_per_second: float = 0.0
35
+ steady_heavy_denoise_per_second: float = 0.0
36
+ tokens_per_second: float = 0.0
37
+
38
+
39
+ @dataclass
40
+ class _RollingState:
41
+ canvas: mx.array
42
+ confidence: mx.array
43
+ entropy: mx.array
44
+ age: mx.array
45
+ latent_state: LatentDeliberationState
46
+ history_hidden_state: mx.array | None
47
+
48
+
49
+ def _flatten_token_ids(*values: object) -> tuple[int, ...]:
50
+ token_ids: list[int] = []
51
+ for value in values:
52
+ if value is None:
53
+ continue
54
+ if isinstance(value, int):
55
+ token_ids.append(int(value))
56
+ elif isinstance(value, (list, tuple, set)):
57
+ token_ids.extend(int(token_id) for token_id in value if token_id is not None)
58
+ return tuple(dict.fromkeys(token_ids))
59
+
60
+
61
+ def _shift_prefix(tensor: mx.array, committed: int, fill_value: float | int) -> mx.array:
62
+ if committed <= 0:
63
+ return tensor
64
+ canvas = tensor.shape[1]
65
+ if committed >= canvas:
66
+ return mx.full(tensor.shape, fill_value, dtype=tensor.dtype)
67
+ kept = tensor[:, committed:]
68
+ fill_shape = (tensor.shape[0], committed, *tensor.shape[2:])
69
+ fill = mx.full(fill_shape, fill_value, dtype=tensor.dtype)
70
+ return mx.concatenate([kept, fill], axis=1)
71
+
72
+
73
+ def _shift_state(
74
+ state: _RollingState,
75
+ committed: int,
76
+ *,
77
+ vocab_size: int,
78
+ canvas_length: int,
79
+ unknown_entropy: float,
80
+ ) -> _RollingState:
81
+ if committed <= 0:
82
+ return state
83
+ tail = mx.random.randint(0, vocab_size, (state.canvas.shape[0], committed))
84
+ canvas = mx.concatenate([state.canvas[:, committed:], tail], axis=1)
85
+ if canvas.shape[1] != canvas_length:
86
+ raise RuntimeError("Canvas shift produced an unexpected length.")
87
+ latent = state.latent_state
88
+ shifted_latent = LatentDeliberationState(
89
+ token_latents=_shift_prefix(latent.token_latents, committed, 0),
90
+ memory_slots=latent.memory_slots,
91
+ confidence=_shift_prefix(latent.confidence, committed, 0),
92
+ entropy=_shift_prefix(latent.entropy, committed, unknown_entropy),
93
+ age=_shift_prefix(latent.age, committed, 0),
94
+ token_changed=_shift_prefix(latent.token_changed, committed, 0),
95
+ confidence_delta=_shift_prefix(latent.confidence_delta, committed, 0),
96
+ entropy_delta=_shift_prefix(latent.entropy_delta, committed, 0),
97
+ ponder_steps=mx.zeros_like(latent.ponder_steps),
98
+ stagnation_steps=mx.zeros_like(latent.stagnation_steps),
99
+ )
100
+ history = state.history_hidden_state
101
+ if history is not None:
102
+ history = _shift_prefix(history, committed, 0)
103
+ return _RollingState(
104
+ canvas=canvas,
105
+ confidence=_shift_prefix(state.confidence, committed, 0),
106
+ entropy=_shift_prefix(state.entropy, committed, unknown_entropy),
107
+ age=_shift_prefix(state.age, committed, 0),
108
+ latent_state=shifted_latent,
109
+ history_hidden_state=history,
110
+ )
111
+
112
+
113
+ def generate(
114
+ model: ModilifyMk1ForBlockDiffusion,
115
+ input_ids: mx.array,
116
+ *,
117
+ max_new_tokens: int | None = None,
118
+ temperature: float | None = None,
119
+ attention_mask: mx.array | None = None,
120
+ pixel_values: mx.array | None = None,
121
+ mm_token_type_ids: mx.array | None = None,
122
+ max_denoising_steps: int | None = None,
123
+ seed: int | None = None,
124
+ profiler=None,
125
+ ) -> ModilifyMk1GenerationOutput:
126
+ """Generate one response with rolling block diffusion.
127
+
128
+ Batch size 1 only. Semantics follow ``generation_modilify_mk1.py``.
129
+ """
130
+
131
+ if input_ids.ndim != 2 or input_ids.shape[0] != 1:
132
+ raise ValueError("Native Mk1 generation currently requires shape [1, sequence].")
133
+ if seed is not None:
134
+ mx.random.seed(int(seed))
135
+
136
+ config = model.config
137
+ canvas_length = int(config.canvas_length)
138
+ vocab_size = int(config.vocab_size)
139
+ dtype = model.model.decoder.embed_tokens.weight.dtype
140
+ max_new = int(max_new_tokens if max_new_tokens is not None else 256)
141
+ if max_new <= 0:
142
+ raise ValueError("`max_new_tokens` must be positive.")
143
+ denoise_temperature = (
144
+ float(config.denoise_temperature) if temperature is None else float(temperature)
145
+ )
146
+ max_iterations = max(1, max_new * int(config.max_ponder_steps))
147
+ unknown_entropy = math.log(vocab_size)
148
+
149
+ if attention_mask is None:
150
+ attention_mask = mx.ones(input_ids.shape, dtype=mx.bool_)
151
+ prefill_started = time.perf_counter()
152
+ cache = model.make_cache(max_size=int(input_ids.shape[1]) + max_new)
153
+ cache = model.prefill(
154
+ input_ids,
155
+ attention_mask=attention_mask,
156
+ cache=cache,
157
+ pixel_values=pixel_values,
158
+ mm_token_type_ids=mm_token_type_ids,
159
+ )
160
+ mx.eval([item for block in cache for item in getattr(block, "state", ())])
161
+ prefill_seconds = time.perf_counter() - prefill_started
162
+ compiled_decoder_step = None
163
+
164
+ latent = LatentDeliberationState.empty(
165
+ batch_size=1,
166
+ canvas_length=canvas_length,
167
+ latent_dim=config.latent_dim,
168
+ memory_slots=config.latent_memory_slots,
169
+ dtype=dtype,
170
+ )
171
+ state = _RollingState(
172
+ canvas=mx.random.randint(0, vocab_size, (1, canvas_length)),
173
+ confidence=mx.zeros((1, canvas_length), dtype=mx.float32),
174
+ entropy=mx.full((1, canvas_length), unknown_entropy, dtype=mx.float32),
175
+ age=mx.zeros((1, canvas_length), dtype=mx.int32),
176
+ latent_state=latent,
177
+ history_hidden_state=None,
178
+ )
179
+ stop_token_ids = _flatten_token_ids(config.turn_end_token_id, config.eos_token_id)
180
+ turn_end = int(config.turn_end_token_id)
181
+ generated: list[int] = []
182
+ denoise_steps = 0
183
+ jumps = 0
184
+ shifts = 0
185
+ stop_reason = "episode_watchdog"
186
+ prompt_ids = [int(token) for token in input_ids[0].tolist()]
187
+ generate_started = time.perf_counter()
188
+ first_denoise_seconds = 0.0
189
+ always_active = mx.array([True])
190
+
191
+ with wired_limit(model, None):
192
+ while len(generated) < max_new:
193
+ step_started = time.perf_counter()
194
+ remaining = mx.array([max_new - len(generated)], dtype=mx.int32)
195
+ output = model(
196
+ decoder_input_ids=state.canvas,
197
+ cache=cache,
198
+ previous_confidence=state.confidence,
199
+ previous_entropy=state.entropy,
200
+ token_age=state.age,
201
+ latent_state=state.latent_state,
202
+ history_hidden_state=state.history_hidden_state,
203
+ return_proposal_statistics=True,
204
+ denoise_temperature=denoise_temperature,
205
+ repetition_penalty=float(config.repetition_penalty),
206
+ compiled_decoder_step=None,
207
+ profiler=profiler,
208
+ )
209
+ denoise_steps += 1
210
+ proposal = output.proposal
211
+ proposal_confidence = output.proposal_confidence
212
+ token_entropy = output.token_entropy
213
+ greedy_proposal = output.greedy_proposal
214
+ greedy_confidence = output.greedy_confidence
215
+ next_canvas = proposal
216
+ next_confidence = proposal_confidence.astype(mx.float32)
217
+ next_entropy = token_entropy.astype(mx.float32)
218
+ next_latent = replace(
219
+ output.next_latent_state,
220
+ confidence=next_confidence,
221
+ entropy=next_entropy,
222
+ age=state.age + 1,
223
+ token_changed=(next_canvas != state.canvas).astype(mx.float32),
224
+ confidence_delta=next_confidence - state.confidence,
225
+ entropy_delta=next_entropy - state.entropy,
226
+ )
227
+ next_state = _RollingState(
228
+ canvas=next_canvas,
229
+ confidence=next_confidence,
230
+ entropy=next_entropy,
231
+ age=state.age + 1,
232
+ latent_state=next_latent,
233
+ history_hidden_state=output.heavy_hidden_state,
234
+ )
235
+ if profiler is not None:
236
+ commit_span = profiler.measure(
237
+ "commit",
238
+ proposal,
239
+ proposal_confidence,
240
+ token_entropy,
241
+ )
242
+ policy = select_commit_lengths(
243
+ sampled_token_ids=proposal,
244
+ normal_failure_rate=fused_commit_failure_rate(
245
+ proposal_confidence, token_entropy, vocab_size=vocab_size
246
+ ),
247
+ previous_failure_rate=fused_commit_failure_rate(
248
+ state.confidence, state.entropy, vocab_size=vocab_size
249
+ ),
250
+ greedy_token_ids=greedy_proposal,
251
+ jump_failure_rate=fused_commit_failure_rate(
252
+ greedy_confidence, token_entropy, vocab_size=vocab_size
253
+ ),
254
+ ponder_steps=state.latent_state.ponder_steps,
255
+ stagnation_steps=state.latent_state.stagnation_steps,
256
+ active_rows=always_active,
257
+ remaining_lengths=remaining,
258
+ failure_budget=float(config.commit_failure_budget),
259
+ jump_failure_budget=float(config.jump_failure_budget),
260
+ stop_token_id=stop_token_ids,
261
+ max_ponder_steps=int(config.max_ponder_steps),
262
+ stagnation_threshold=int(config.jump_on_no_progress_after),
263
+ min_progress=float(config.min_trajectory_progress),
264
+ )
265
+ if profiler is not None:
266
+ commit_span.done(policy.commit_lengths, policy.jump_rows)
267
+ if profiler is not None:
268
+ span = profiler.measure(
269
+ "sync",
270
+ policy.commit_lengths,
271
+ policy.jump_rows,
272
+ policy.commit_token_ids,
273
+ )
274
+ mx.eval(
275
+ policy.commit_lengths,
276
+ policy.jump_rows,
277
+ policy.commit_token_ids,
278
+ )
279
+ commit_len = int(policy.commit_lengths[0].item())
280
+ jump = bool(policy.jump_rows[0].item())
281
+ if profiler is not None:
282
+ span.done()
283
+ next_state = replace(
284
+ next_state,
285
+ latent_state=replace(
286
+ next_state.latent_state,
287
+ ponder_steps=policy.ponder_steps,
288
+ stagnation_steps=policy.stagnation_steps,
289
+ ),
290
+ )
291
+ if jump:
292
+ next_state = replace(next_state, canvas=policy.commit_token_ids)
293
+ jumps += 1
294
+ committed_ids: list[int] = []
295
+ if commit_len:
296
+ committed_ids = [
297
+ int(token)
298
+ for token in policy.commit_token_ids[0, :commit_len].tolist()
299
+ ]
300
+ generated.extend(committed_ids)
301
+ committed_block = policy.commit_token_ids[:, :commit_len]
302
+ if profiler is not None:
303
+ span = profiler.measure("update_cache", committed_block)
304
+ cache = model.update_cache(committed_block, cache=cache)
305
+ mx.eval(
306
+ *[
307
+ block.keys
308
+ for block in cache
309
+ if getattr(block, "keys", None) is not None
310
+ ],
311
+ *[
312
+ block.values
313
+ for block in cache
314
+ if getattr(block, "values", None) is not None
315
+ ],
316
+ )
317
+ if profiler is not None:
318
+ span.done()
319
+ shifts += 1
320
+ state = _shift_state(
321
+ next_state,
322
+ commit_len,
323
+ vocab_size=vocab_size,
324
+ canvas_length=canvas_length,
325
+ unknown_entropy=unknown_entropy,
326
+ )
327
+ if profiler is not None:
328
+ profiler.finish_step()
329
+ if denoise_steps == 1:
330
+ first_denoise_seconds = time.perf_counter() - step_started
331
+ generate_started = time.perf_counter()
332
+ if committed_ids:
333
+ if turn_end in committed_ids:
334
+ stop_reason = "turn_end"
335
+ break
336
+ if any(
337
+ token in stop_token_ids and token != turn_end
338
+ for token in committed_ids
339
+ ):
340
+ stop_reason = "eos"
341
+ break
342
+ if len(generated) >= max_new:
343
+ stop_reason = "max_new_tokens"
344
+ break
345
+ if (
346
+ max_denoising_steps is not None
347
+ and denoise_steps >= max_denoising_steps
348
+ ):
349
+ stop_reason = "max_denoising_steps"
350
+ break
351
+ if denoise_steps >= max_iterations:
352
+ stop_reason = "episode_watchdog"
353
+ break
354
+
355
+ generate_seconds = time.perf_counter() - generate_started
356
+ if denoise_steps <= 1:
357
+ generate_seconds = first_denoise_seconds
358
+ steady_hdps = 0.0
359
+ overall_hdps = (1.0 / first_denoise_seconds) if first_denoise_seconds else 0.0
360
+ tokens_per_second = (
361
+ len(generated) / first_denoise_seconds if first_denoise_seconds else 0.0
362
+ )
363
+ else:
364
+ steady_hdps = (denoise_steps - 1) / max(generate_seconds, 1e-9)
365
+ overall_hdps = denoise_steps / max(
366
+ first_denoise_seconds + generate_seconds, 1e-9
367
+ )
368
+ tokens_per_second = len(generated) / max(
369
+ first_denoise_seconds + generate_seconds, 1e-9
370
+ )
371
+ average_commit = (len(generated) / shifts) if shifts else 0.0
372
+ tpf = (len(generated) / denoise_steps) if denoise_steps else 0.0
373
+ return ModilifyMk1GenerationOutput(
374
+ sequences=prompt_ids + generated,
375
+ generated_ids=generated,
376
+ generated_length=len(generated),
377
+ denoise_steps=denoise_steps,
378
+ jump_count=jumps,
379
+ average_commit_len=average_commit,
380
+ stop_reason=stop_reason,
381
+ tokens_per_forward=tpf,
382
+ prefill_seconds=prefill_seconds,
383
+ generate_seconds=first_denoise_seconds + generate_seconds,
384
+ first_denoise_seconds=first_denoise_seconds,
385
+ heavy_denoise_per_second=overall_hdps,
386
+ steady_heavy_denoise_per_second=steady_hdps,
387
+ tokens_per_second=tokens_per_second,
388
+ )
modilify_mlx/language.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Mk1 wrappers around the borrowed DiffusionGemma trunk layers.
4
+
5
+ The trunk implementation is imported as an internal dependency. The public
6
+ model type remains ``modilify_mk1``. Two Mk1-specific forwards are installed
7
+ on the constructed trunk:
8
+
9
+ * Router: softmax over all experts, then top-k and renormalize
10
+ * Canvas merge: RMS-capped latent residual, not previous-logit soft embeds
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import mlx.core as mx
16
+ import mlx.nn as nn
17
+
18
+ from mlx_vlm.models.diffusion_gemma.language import (
19
+ DiffusionGemma4Backbone,
20
+ Router,
21
+ geglu,
22
+ )
23
+
24
+ LATENT_RESIDUAL_RMS_RATIO_CAP = 0.5
25
+
26
+
27
+ class Mk1Router(Router):
28
+ """Official router weights with Mk1 log-softmax / top-k routing."""
29
+
30
+ def __call__(self, x: mx.array) -> tuple[mx.array, mx.array]:
31
+ x = mx.fast.rms_norm(x, None, self.eps)
32
+ x = x * self.scale * self._root_size
33
+ scores = self.proj(x)
34
+ probabilities = mx.softmax(scores, axis=-1, precise=True)
35
+ top_k = self.config.top_k_experts
36
+ indices = mx.argpartition(probabilities, kth=-top_k, axis=-1)[..., -top_k:]
37
+ weights = mx.take_along_axis(probabilities, indices, axis=-1)
38
+ weights = weights / mx.sum(weights, axis=-1, keepdims=True)
39
+ weights = weights * self.per_expert_scale[indices]
40
+ return indices, weights
41
+
42
+
43
+ def merge_latent_context(
44
+ mapper: nn.Module,
45
+ token_embeddings: mx.array,
46
+ latent_context: mx.array | None,
47
+ *,
48
+ rms_ratio_cap: float = LATENT_RESIDUAL_RMS_RATIO_CAP,
49
+ ) -> mx.array:
50
+ """Apply the native self-conditioning bridge with Mk1 RMS capping."""
51
+
52
+ context = (
53
+ mx.zeros_like(token_embeddings)
54
+ if latent_context is None
55
+ else latent_context.astype(token_embeddings.dtype)
56
+ )
57
+ if context.shape != token_embeddings.shape:
58
+ raise ValueError("Latent context must match the canvas embedding shape.")
59
+ normalized = mapper.pre_norm(context)
60
+ mapped = mapper.down_proj(
61
+ geglu(mapper.gate_proj(normalized), mapper.up_proj(normalized))
62
+ )
63
+ mapped_rms = mx.sqrt(
64
+ mx.mean(mx.square(mapped.astype(mx.float32)), axis=-1, keepdims=True)
65
+ )
66
+ token_rms = mx.sqrt(
67
+ mx.mean(
68
+ mx.square(token_embeddings.astype(mx.float32)), axis=-1, keepdims=True
69
+ )
70
+ )
71
+ cap = rms_ratio_cap * token_rms
72
+ scale = cap / mx.sqrt(mx.square(mapped_rms) + mx.square(cap) + 1.0e-12)
73
+ mapped = mapped * scale.astype(mapped.dtype)
74
+ return mapper.post_norm(token_embeddings + mapped)
75
+
76
+
77
+ def _install_mk1_embed_canvas(decoder: nn.Module) -> None:
78
+ def _embed_canvas(
79
+ canvas_ids,
80
+ self_conditioning_logits=None,
81
+ self_conditioning_embeddings=None,
82
+ ):
83
+ if self_conditioning_logits is not None:
84
+ raise ValueError(
85
+ "Modilify Mk1 uses latent embeddings, not logits self-conditioning."
86
+ )
87
+ token_embeddings = decoder.embed_tokens(canvas_ids) * decoder.embed_scale
88
+ return merge_latent_context(
89
+ decoder.self_conditioning,
90
+ token_embeddings,
91
+ self_conditioning_embeddings,
92
+ )
93
+
94
+ decoder._embed_canvas = _embed_canvas
95
+
96
+
97
+ def _install_mk1_routers(backbone: DiffusionGemma4Backbone) -> None:
98
+ for layer in backbone.decoder.layers:
99
+ replacement = Mk1Router(layer.router.config)
100
+ replacement.update(layer.router.parameters())
101
+ layer.router = replacement
102
+
103
+
104
+ def build_mk1_backbone(trunk_config) -> DiffusionGemma4Backbone:
105
+ """Construct the borrowed trunk and install Mk1 forwards."""
106
+
107
+ backbone = DiffusionGemma4Backbone(trunk_config)
108
+ _install_mk1_routers(backbone)
109
+ _install_mk1_embed_canvas(backbone.decoder)
110
+ return backbone
modilify_mlx/latent_deliberation.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Fixed-shape latent deliberation state for Modilify Mk1 decoding.
4
+
5
+ PyTorch → MLX:
6
+ nn.LayerNorm mlx.nn.LayerNorm
7
+ nn.Linear mlx.nn.Linear
8
+ nn.MultiheadAttention in_proj BiasedMHA query/key/value_proj (split QKV)
9
+ nn.Sequential token_ff/memory_ff mlx.nn.Sequential (layers.0 / layers.2)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ import math
16
+
17
+ import mlx.core as mx
18
+ import mlx.nn as nn
19
+
20
+
21
+ @dataclass
22
+ class LatentDeliberationState:
23
+ """Persistent, fixed-size state for one or more canvas episodes."""
24
+
25
+ token_latents: mx.array
26
+ memory_slots: mx.array
27
+ confidence: mx.array
28
+ entropy: mx.array
29
+ age: mx.array
30
+ token_changed: mx.array
31
+ confidence_delta: mx.array
32
+ entropy_delta: mx.array
33
+ ponder_steps: mx.array
34
+ stagnation_steps: mx.array
35
+
36
+ @classmethod
37
+ def empty(
38
+ cls,
39
+ *,
40
+ batch_size: int,
41
+ canvas_length: int,
42
+ latent_dim: int,
43
+ memory_slots: int,
44
+ dtype: mx.Dtype,
45
+ ) -> "LatentDeliberationState":
46
+ """Create a zero-initialized recurrent state."""
47
+
48
+ return cls(
49
+ token_latents=mx.zeros((batch_size, canvas_length, latent_dim), dtype=dtype),
50
+ memory_slots=mx.zeros((batch_size, memory_slots, latent_dim), dtype=dtype),
51
+ confidence=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
52
+ entropy=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
53
+ age=mx.zeros((batch_size, canvas_length), dtype=mx.int32),
54
+ token_changed=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
55
+ confidence_delta=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
56
+ entropy_delta=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
57
+ ponder_steps=mx.zeros((batch_size,), dtype=mx.int32),
58
+ stagnation_steps=mx.zeros((batch_size,), dtype=mx.int32),
59
+ )
60
+
61
+ def shift(
62
+ self, committed: int, *, entropy_fill_value: float = 0.0
63
+ ) -> "LatentDeliberationState":
64
+ """Drop committed canvas positions without changing long-term memory."""
65
+
66
+ canvas_length = self.token_latents.shape[1]
67
+ if not 0 <= committed <= canvas_length:
68
+ raise ValueError("`committed` must be in [0, canvas_length].")
69
+ if committed == 0:
70
+ return self
71
+
72
+ def shifted(tensor: mx.array, fill_value: float | int = 0) -> mx.array:
73
+ result = mx.full(tensor.shape, fill_value, dtype=tensor.dtype)
74
+ if committed < canvas_length:
75
+ kept = tensor[:, committed:]
76
+ result = mx.concatenate(
77
+ [kept, mx.full(
78
+ (tensor.shape[0], committed, *tensor.shape[2:]),
79
+ fill_value,
80
+ dtype=tensor.dtype,
81
+ )],
82
+ axis=1,
83
+ ) if tensor.ndim > 2 else mx.concatenate(
84
+ [
85
+ kept,
86
+ mx.full(
87
+ (tensor.shape[0], committed),
88
+ fill_value,
89
+ dtype=tensor.dtype,
90
+ ),
91
+ ],
92
+ axis=1,
93
+ )
94
+ return result
95
+
96
+ return LatentDeliberationState(
97
+ token_latents=shifted(self.token_latents),
98
+ memory_slots=self.memory_slots,
99
+ confidence=shifted(self.confidence),
100
+ entropy=shifted(self.entropy, entropy_fill_value),
101
+ age=shifted(self.age),
102
+ token_changed=shifted(self.token_changed),
103
+ confidence_delta=shifted(self.confidence_delta),
104
+ entropy_delta=shifted(self.entropy_delta),
105
+ ponder_steps=mx.zeros_like(self.ponder_steps),
106
+ stagnation_steps=mx.zeros_like(self.stagnation_steps),
107
+ )
108
+
109
+
110
+ def advance_trajectory_clocks(
111
+ ponder_steps: mx.array,
112
+ stagnation_steps: mx.array,
113
+ *,
114
+ commit_lengths: mx.array,
115
+ active_rows: mx.array,
116
+ progress_scores: mx.array,
117
+ min_progress: float,
118
+ ) -> tuple[mx.array, mx.array]:
119
+ """Advance useful-ponder and true-stagnation clocks for each row."""
120
+
121
+ if min_progress < 0:
122
+ raise ValueError("`min_progress` must be non-negative.")
123
+ if not (
124
+ ponder_steps.shape
125
+ == stagnation_steps.shape
126
+ == commit_lengths.shape
127
+ == active_rows.shape
128
+ == progress_scores.shape
129
+ ):
130
+ raise ValueError("Trajectory clock inputs must share shape [batch].")
131
+ committed = commit_lengths > 0
132
+ waiting = active_rows & (~committed)
133
+ improving = progress_scores >= min_progress
134
+ next_ponder = mx.where(
135
+ committed,
136
+ mx.zeros_like(ponder_steps),
137
+ ponder_steps + waiting.astype(mx.int32),
138
+ )
139
+ next_stagnation = mx.where(
140
+ committed,
141
+ mx.zeros_like(stagnation_steps),
142
+ mx.where(
143
+ waiting & improving,
144
+ mx.zeros_like(stagnation_steps),
145
+ stagnation_steps + waiting.astype(mx.int32),
146
+ ),
147
+ )
148
+ return next_ponder.astype(mx.int32), next_stagnation.astype(mx.int32)
149
+
150
+
151
+ def should_force_trajectory_jump(
152
+ ponder_steps: mx.array,
153
+ stagnation_steps: mx.array,
154
+ *,
155
+ max_ponder_steps: int,
156
+ stagnation_threshold: int,
157
+ ) -> mx.array:
158
+ """Return whether a row has exhausted ponder or stagnation budget."""
159
+
160
+ if max_ponder_steps <= 0 or stagnation_threshold <= 0:
161
+ raise ValueError("Trajectory jump limits must be positive.")
162
+ return (ponder_steps >= max_ponder_steps) | (
163
+ stagnation_steps >= stagnation_threshold
164
+ )
165
+
166
+
167
+ class BiasedMHA(nn.Module):
168
+ """Explicit QKV attention matching PyTorch MultiheadAttention weights."""
169
+
170
+ def __init__(self, dims: int, num_heads: int) -> None:
171
+ super().__init__()
172
+ if dims % num_heads:
173
+ raise ValueError("`dims` must be divisible by `num_heads`.")
174
+ self.num_heads = num_heads
175
+ self.head_dim = dims // num_heads
176
+ self.scale = self.head_dim**-0.5
177
+ self.query_proj = nn.Linear(dims, dims, bias=True)
178
+ self.key_proj = nn.Linear(dims, dims, bias=True)
179
+ self.value_proj = nn.Linear(dims, dims, bias=True)
180
+ self.out_proj = nn.Linear(dims, dims, bias=True)
181
+
182
+ def __call__(
183
+ self,
184
+ queries: mx.array,
185
+ keys: mx.array,
186
+ values: mx.array,
187
+ mask: mx.array | None = None,
188
+ ) -> mx.array:
189
+ batch, query_len, dims = queries.shape
190
+ key_len = keys.shape[1]
191
+ queries = self.query_proj(queries).reshape(
192
+ batch, query_len, self.num_heads, self.head_dim
193
+ ).transpose(0, 2, 1, 3)
194
+ keys = self.key_proj(keys).reshape(
195
+ batch, key_len, self.num_heads, self.head_dim
196
+ ).transpose(0, 2, 1, 3)
197
+ values = self.value_proj(values).reshape(
198
+ batch, key_len, self.num_heads, self.head_dim
199
+ ).transpose(0, 2, 1, 3)
200
+ attn_mask = None
201
+ if mask is not None:
202
+ attn_mask = mask.reshape(1, 1, mask.shape[-2], mask.shape[-1])
203
+ output = mx.fast.scaled_dot_product_attention(
204
+ queries, keys, values, scale=self.scale, mask=attn_mask
205
+ )
206
+ output = output.transpose(0, 2, 1, 3).reshape(batch, query_len, dims)
207
+ return self.out_proj(output)
208
+
209
+
210
+ def _silu(x: mx.array) -> mx.array:
211
+ return x * mx.sigmoid(x)
212
+
213
+
214
+ class _TemporalTransformerCell(nn.Module):
215
+ """One-step recurrent token update with fixed-slot memory attention."""
216
+
217
+ def __init__(
218
+ self,
219
+ latent_dim: int,
220
+ num_heads: int,
221
+ local_attention_window: int,
222
+ ) -> None:
223
+ super().__init__()
224
+ self.state_norm = nn.LayerNorm(latent_dim)
225
+ self.observation_norm = nn.LayerNorm(latent_dim)
226
+ self.memory_address_norm = nn.LayerNorm(latent_dim)
227
+ self.memory_value_norm = nn.LayerNorm(latent_dim)
228
+ self.temporal_update = nn.Linear(2 * latent_dim, 2 * latent_dim)
229
+ self.local_attention = BiasedMHA(latent_dim, num_heads)
230
+ self.local_attention_window = local_attention_window
231
+ self.token_memory_attention = BiasedMHA(latent_dim, num_heads)
232
+ self.memory_token_attention = BiasedMHA(latent_dim, num_heads)
233
+ self.token_ff_norm = nn.LayerNorm(latent_dim)
234
+ self.memory_ff_norm = nn.LayerNorm(latent_dim)
235
+ self.stored_token_norm = nn.LayerNorm(latent_dim)
236
+ self.stored_memory_norm = nn.LayerNorm(latent_dim)
237
+ expansion = latent_dim * 4
238
+ self.token_ff = nn.Sequential(
239
+ nn.Linear(latent_dim, expansion),
240
+ nn.SiLU(),
241
+ nn.Linear(expansion, latent_dim),
242
+ )
243
+ self.memory_ff = nn.Sequential(
244
+ nn.Linear(latent_dim, expansion),
245
+ nn.SiLU(),
246
+ nn.Linear(expansion, latent_dim),
247
+ )
248
+
249
+ def _local_mask(self, length: int, dtype: mx.Dtype) -> mx.array:
250
+ positions = mx.arange(length)
251
+ allowed = (
252
+ mx.abs(positions[:, None] - positions[None, :]) < self.local_attention_window
253
+ )
254
+ return mx.where(
255
+ allowed,
256
+ mx.zeros((length, length), dtype=dtype),
257
+ mx.full((length, length), mx.finfo(dtype).min, dtype=dtype),
258
+ )
259
+
260
+ def __call__(
261
+ self,
262
+ previous_tokens: mx.array,
263
+ observation: mx.array,
264
+ memory: mx.array,
265
+ memory_slot_identity: mx.array,
266
+ ) -> tuple[mx.array, mx.array]:
267
+ gate_logits, candidate = mx.split(
268
+ self.temporal_update(
269
+ mx.concatenate(
270
+ (
271
+ self.state_norm(previous_tokens),
272
+ self.observation_norm(observation),
273
+ ),
274
+ axis=-1,
275
+ )
276
+ ),
277
+ 2,
278
+ axis=-1,
279
+ )
280
+ gate = mx.sigmoid(gate_logits)
281
+ tokens = gate * previous_tokens + (1.0 - gate) * _silu(candidate)
282
+ local_mask = self._local_mask(tokens.shape[1], tokens.dtype)
283
+ normed_tokens = self.state_norm(tokens)
284
+ tokens = tokens + self.local_attention(
285
+ normed_tokens, normed_tokens, normed_tokens, mask=local_mask
286
+ )
287
+
288
+ addressed_memory = self.memory_address_norm(memory + memory_slot_identity)
289
+ memory_values = self.memory_value_norm(memory)
290
+ normed_tokens = self.state_norm(tokens)
291
+ tokens = tokens + self.token_memory_attention(
292
+ normed_tokens, addressed_memory, memory_values
293
+ )
294
+ tokens = tokens + self.token_ff(self.token_ff_norm(tokens))
295
+
296
+ normed_tokens = self.state_norm(tokens)
297
+ memory = memory + self.memory_token_attention(
298
+ addressed_memory, normed_tokens, normed_tokens
299
+ )
300
+ memory = memory + self.memory_ff(self.memory_ff_norm(memory))
301
+ return (
302
+ self.stored_token_norm(tokens),
303
+ self.stored_memory_norm(memory) + memory_slot_identity,
304
+ )
305
+
306
+
307
+ class LatentDeliberationTransformer(nn.Module):
308
+ """Small recurrent Transformer that compresses repeated denoise context."""
309
+
310
+ def __init__(
311
+ self,
312
+ *,
313
+ hidden_size: int,
314
+ latent_dim: int = 512,
315
+ memory_slots: int = 16,
316
+ num_layers: int = 2,
317
+ num_heads: int = 8,
318
+ local_attention_window: int = 32,
319
+ dropout: float = 0.0,
320
+ ) -> None:
321
+ super().__init__()
322
+ del dropout
323
+ if latent_dim % num_heads:
324
+ raise ValueError("`latent_dim` must be divisible by `num_heads`.")
325
+ if local_attention_window <= 0:
326
+ raise ValueError("`local_attention_window` must be positive.")
327
+ self.hidden_size = hidden_size
328
+ self.latent_dim = latent_dim
329
+ self.memory_slots = memory_slots
330
+ self.heavy_projection = nn.Linear(hidden_size, latent_dim, bias=False)
331
+ self.embedding_projection = nn.Linear(hidden_size, latent_dim, bias=False)
332
+ self.scalar_projection = nn.Linear(11, latent_dim, bias=False)
333
+ self.blocks = [
334
+ _TemporalTransformerCell(latent_dim, num_heads, local_attention_window)
335
+ for _ in range(num_layers)
336
+ ]
337
+ self.output_norm = nn.LayerNorm(latent_dim)
338
+ self.output_projection = nn.Linear(latent_dim, hidden_size, bias=False)
339
+ self.memory_slot_identity = mx.zeros((memory_slots, latent_dim))
340
+
341
+ def scaled_memory_slot_identity(
342
+ self,
343
+ *,
344
+ batch_size: int,
345
+ dtype: mx.Dtype,
346
+ ) -> mx.array:
347
+ """Return unit directions scaled to LayerNorm RMS."""
348
+
349
+ identity = self.memory_slot_identity.astype(mx.float32)
350
+ identity = identity / mx.maximum(
351
+ mx.linalg.norm(identity, axis=-1, keepdims=True), 1.0e-12
352
+ )
353
+ identity = identity * math.sqrt(self.latent_dim)
354
+ return mx.broadcast_to(
355
+ identity.astype(dtype)[None, :, :],
356
+ (batch_size, self.memory_slots, self.latent_dim),
357
+ )
358
+
359
+ def project_context(self, token_latents: mx.array) -> mx.array:
360
+ """Translate latent state into a self-conditioning embedding."""
361
+
362
+ return self.output_projection(self.output_norm(token_latents))
363
+
364
+ def __call__(
365
+ self,
366
+ *,
367
+ heavy_hidden: mx.array,
368
+ token_embeddings: mx.array,
369
+ confidence: mx.array,
370
+ entropy: mx.array,
371
+ state: LatentDeliberationState,
372
+ ) -> tuple[mx.array, LatentDeliberationState]:
373
+ """Advance latent memory and produce decoder self-conditioning."""
374
+
375
+ if heavy_hidden.ndim != 3:
376
+ raise ValueError("`heavy_hidden` must have shape [batch, canvas, hidden].")
377
+ if heavy_hidden.shape != token_embeddings.shape:
378
+ raise ValueError(
379
+ "`heavy_hidden` and `token_embeddings` must have the same shape."
380
+ )
381
+ batch_size, canvas_length, hidden_size = heavy_hidden.shape
382
+ if hidden_size != self.hidden_size:
383
+ raise ValueError("Unexpected hidden size for latent deliberation.")
384
+ expected_state = (batch_size, canvas_length, self.latent_dim)
385
+ if state.token_latents.shape != expected_state:
386
+ raise ValueError("State token latents do not match the current canvas.")
387
+ if state.memory_slots.shape != (
388
+ batch_size,
389
+ self.memory_slots,
390
+ self.latent_dim,
391
+ ):
392
+ raise ValueError("State memory slots do not match this module.")
393
+
394
+ dtype = heavy_hidden.dtype
395
+ position = mx.linspace(-1.0, 1.0, canvas_length, dtype=dtype)
396
+ position = mx.broadcast_to(position[None, :], (batch_size, canvas_length))
397
+ age = mx.minimum(state.age.astype(dtype), 32767).astype(dtype)
398
+ entropy_delta = state.entropy_delta.astype(dtype)
399
+ scalars = mx.stack(
400
+ (
401
+ confidence.astype(dtype),
402
+ mx.log1p(entropy.astype(dtype)),
403
+ mx.log1p(age),
404
+ position,
405
+ state.token_changed.astype(dtype),
406
+ state.confidence_delta.astype(dtype),
407
+ mx.sign(entropy_delta) * mx.log1p(mx.abs(entropy_delta)),
408
+ mx.broadcast_to(
409
+ mx.log1p(state.ponder_steps.astype(dtype))[:, None],
410
+ (batch_size, canvas_length),
411
+ ),
412
+ mx.broadcast_to(
413
+ mx.log1p(state.stagnation_steps.astype(dtype))[:, None],
414
+ (batch_size, canvas_length),
415
+ ),
416
+ confidence.astype(dtype)
417
+ * mx.exp(-mx.maximum(entropy.astype(dtype), 0.0)),
418
+ mx.maximum(state.confidence_delta.astype(dtype), 0.0)
419
+ + mx.log1p(mx.maximum(-entropy_delta, 0.0)),
420
+ ),
421
+ axis=-1,
422
+ )
423
+ observation = (
424
+ self.heavy_projection(heavy_hidden)
425
+ + self.embedding_projection(token_embeddings)
426
+ + self.scalar_projection(scalars)
427
+ )
428
+ tokens = state.token_latents
429
+ memory = state.memory_slots
430
+ slot_identity = self.scaled_memory_slot_identity(
431
+ batch_size=batch_size,
432
+ dtype=memory.dtype,
433
+ )
434
+ for block in self.blocks:
435
+ tokens, memory = block(tokens, observation, memory, slot_identity)
436
+ observation = tokens
437
+ next_state = LatentDeliberationState(
438
+ token_latents=tokens,
439
+ memory_slots=memory,
440
+ confidence=confidence.astype(mx.float32),
441
+ entropy=entropy.astype(mx.float32),
442
+ age=state.age,
443
+ token_changed=state.token_changed,
444
+ confidence_delta=state.confidence_delta,
445
+ entropy_delta=state.entropy_delta,
446
+ ponder_steps=state.ponder_steps,
447
+ stagnation_steps=state.stagnation_steps,
448
+ )
449
+ return self.project_context(tokens), next_state
modilify_mlx/modeling.py ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Native MLX Modilify Mk1 model."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, replace
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import mlx.core as mx
12
+ import mlx.nn as nn
13
+ from mlx.utils import tree_flatten
14
+
15
+ from .config import MODEL_TYPE, ModilifyMk1Config
16
+ from .convert_utils import remap_state_dict
17
+ from .fast_decode import (
18
+ decoder_hidden_states,
19
+ make_compiled_attn_layers,
20
+ quantize_experts,
21
+ )
22
+ from .language import build_mk1_backbone
23
+ from .latent_deliberation import LatentDeliberationState, LatentDeliberationTransformer
24
+
25
+
26
+ @dataclass
27
+ class ModilifyMk1StepOutput:
28
+ """One heavy-denoise step over the rolling canvas."""
29
+
30
+ logits: mx.array | None
31
+ heavy_hidden_state: mx.array
32
+ next_latent_state: LatentDeliberationState
33
+ cache: Any
34
+ latent_context: mx.array
35
+ proposal: mx.array | None = None
36
+ proposal_confidence: mx.array | None = None
37
+ token_entropy: mx.array | None = None
38
+ greedy_proposal: mx.array | None = None
39
+ greedy_confidence: mx.array | None = None
40
+
41
+
42
+ def _softcap(logits: mx.array, cap: float) -> mx.array:
43
+ return mx.tanh(logits.astype(mx.float32) / cap) * cap
44
+
45
+
46
+ def _softmax_statistics(
47
+ logits: mx.array,
48
+ temperature: float,
49
+ ) -> tuple[mx.array, mx.array, mx.array, mx.array, mx.array]:
50
+ scores = logits.astype(mx.float32) / temperature
51
+ probabilities = mx.softmax(scores, axis=-1, precise=True)
52
+ greedy_proposal = mx.argmax(probabilities, axis=-1)
53
+ greedy_confidence = mx.squeeze(
54
+ mx.take_along_axis(probabilities, greedy_proposal[..., None], axis=-1),
55
+ axis=-1,
56
+ )
57
+ token_entropy = -mx.sum(
58
+ probabilities * mx.log(mx.maximum(probabilities, 1.0e-30)),
59
+ axis=-1,
60
+ )
61
+ return scores, probabilities, greedy_proposal, greedy_confidence, token_entropy
62
+
63
+
64
+ _compiled_softmax_statistics = mx.compile(_softmax_statistics, shapeless=True)
65
+
66
+
67
+ class ModilifyMk1ForBlockDiffusion(nn.Module):
68
+ """Inference-only multimodal Modilify Mk1 model."""
69
+
70
+ def __init__(self, config: ModilifyMk1Config) -> None:
71
+ super().__init__()
72
+ if config.model_type != MODEL_TYPE:
73
+ raise ValueError(
74
+ f"Refusing to construct Mk1 with model_type={config.model_type!r}."
75
+ )
76
+ self.config = config
77
+ self.model = build_mk1_backbone(config.trunk_model_config())
78
+ self.latent_deliberation = LatentDeliberationTransformer(
79
+ hidden_size=config.hidden_size,
80
+ latent_dim=config.latent_dim,
81
+ memory_slots=config.latent_memory_slots,
82
+ num_layers=config.latent_num_layers,
83
+ num_heads=config.latent_num_heads,
84
+ local_attention_window=config.latent_local_attention_window,
85
+ dropout=config.latent_dropout,
86
+ )
87
+ self.final_logit_softcapping = float(
88
+ config.text_config.final_logit_softcapping
89
+ )
90
+ self._decoder_compile_failed = False
91
+ self._compiled_attn_layers = None
92
+
93
+ def make_cache(self, max_size: int | None = None):
94
+ return self.model.encoder.make_cache(max_size=max_size)
95
+
96
+ def embed_canvas_tokens(self, decoder_input_ids: mx.array) -> mx.array:
97
+ return (
98
+ self.model.decoder.embed_tokens(decoder_input_ids)
99
+ * self.model.decoder.embed_scale
100
+ )
101
+
102
+ def prefill(
103
+ self,
104
+ input_ids: mx.array,
105
+ *,
106
+ attention_mask: mx.array | None = None,
107
+ cache=None,
108
+ pixel_values: mx.array | None = None,
109
+ mm_token_type_ids: mx.array | None = None,
110
+ ):
111
+ if cache is None:
112
+ cache = self.make_cache()
113
+ _, cache = self.model.encoder(
114
+ input_ids,
115
+ attention_mask=attention_mask,
116
+ cache=cache,
117
+ pixel_values=pixel_values,
118
+ mm_token_type_ids=mm_token_type_ids,
119
+ )
120
+ return cache
121
+
122
+ def update_cache(self, input_ids: mx.array, *, cache, attention_mask=None):
123
+ _, cache = self.model.encoder(
124
+ input_ids,
125
+ attention_mask=attention_mask,
126
+ cache=cache,
127
+ )
128
+ return cache
129
+
130
+ def _prepare_latent_context(
131
+ self,
132
+ decoder_input_ids: mx.array,
133
+ *,
134
+ history_hidden_state: mx.array | None,
135
+ confidence: mx.array | None,
136
+ entropy: mx.array | None,
137
+ age: mx.array | None,
138
+ latent_state: LatentDeliberationState | None,
139
+ dtype: mx.Dtype,
140
+ ) -> tuple[mx.array, LatentDeliberationState]:
141
+ batch_size, canvas_length = decoder_input_ids.shape
142
+ if latent_state is None:
143
+ latent_state = LatentDeliberationState.empty(
144
+ batch_size=batch_size,
145
+ canvas_length=canvas_length,
146
+ latent_dim=self.config.latent_dim,
147
+ memory_slots=self.config.latent_memory_slots,
148
+ dtype=dtype,
149
+ )
150
+ if confidence is None:
151
+ confidence = latent_state.confidence
152
+ else:
153
+ confidence = mx.squeeze(confidence.astype(mx.float32), axis=-1) if (
154
+ confidence.ndim == 3
155
+ ) else confidence.astype(mx.float32)
156
+ if entropy is None:
157
+ entropy = latent_state.entropy
158
+ else:
159
+ entropy = mx.squeeze(entropy.astype(mx.float32), axis=-1) if (
160
+ entropy.ndim == 3
161
+ ) else entropy.astype(mx.float32)
162
+ if age is not None:
163
+ latent_state = replace(latent_state, age=age.astype(mx.int32))
164
+ token_embeddings = self.embed_canvas_tokens(decoder_input_ids)
165
+ history = (
166
+ mx.zeros_like(token_embeddings)
167
+ if history_hidden_state is None
168
+ else history_hidden_state
169
+ )
170
+ return self.latent_deliberation(
171
+ heavy_hidden=history,
172
+ token_embeddings=token_embeddings,
173
+ confidence=confidence,
174
+ entropy=entropy,
175
+ state=latent_state,
176
+ )
177
+
178
+ def _proposal_statistics(
179
+ self,
180
+ logits: mx.array,
181
+ *,
182
+ denoise_temperature: float | None = None,
183
+ repetition_token_mask: mx.array | None = None,
184
+ repetition_penalty: float = 1.0,
185
+ ) -> tuple[mx.array, mx.array, mx.array, mx.array, mx.array]:
186
+ temperature = (
187
+ self.config.denoise_temperature
188
+ if denoise_temperature is None
189
+ else float(denoise_temperature)
190
+ )
191
+ if temperature <= 0:
192
+ raise ValueError("`denoise_temperature` must be positive.")
193
+ if (
194
+ repetition_token_mask is not None
195
+ and repetition_penalty != 1.0
196
+ and repetition_penalty > 0
197
+ ):
198
+ scores = logits.astype(mx.float32)
199
+ penalized = mx.where(
200
+ scores < 0,
201
+ scores * repetition_penalty,
202
+ scores / repetition_penalty,
203
+ )
204
+ mask = repetition_token_mask.astype(mx.bool_)[:, None, :]
205
+ logits = mx.where(mask, penalized, scores)
206
+ try:
207
+ scores, probabilities, greedy_proposal, greedy_confidence, token_entropy = (
208
+ _compiled_softmax_statistics(logits, temperature)
209
+ )
210
+ except ValueError:
211
+ scores, probabilities, greedy_proposal, greedy_confidence, token_entropy = (
212
+ _softmax_statistics(logits, temperature)
213
+ )
214
+ proposal = mx.random.categorical(scores, axis=-1)
215
+ proposal_confidence = mx.squeeze(
216
+ mx.take_along_axis(probabilities, proposal[..., None], axis=-1),
217
+ axis=-1,
218
+ )
219
+ return (
220
+ proposal,
221
+ proposal_confidence,
222
+ token_entropy,
223
+ greedy_proposal,
224
+ greedy_confidence,
225
+ )
226
+
227
+ def compile_attention(self, cache) -> None:
228
+ """Compile per-layer attention residuals. Expert FFNs stay eager."""
229
+
230
+ from .fast_decode import _cache_capacity, build_decoder_masks
231
+
232
+ print("[mk1] compiling attention layers", flush=True)
233
+ compiled = make_compiled_attn_layers(self.model.decoder, cache)
234
+ prefix_len = int(getattr(cache[0], "offset", 0))
235
+ canvas = int(self.config.canvas_length)
236
+ hidden = int(self.config.hidden_size)
237
+ dtype = self.model.decoder.embed_tokens.weight.dtype
238
+ dummy = mx.zeros((1, canvas, hidden), dtype=dtype)
239
+ offset = mx.array(prefix_len)
240
+ full_mask, slide_mask = build_decoder_masks(
241
+ prefix_len=prefix_len,
242
+ canvas_length=canvas,
243
+ cache_capacity=max(_cache_capacity(cache), 1),
244
+ sliding_window=int(self.config.text_config.sliding_window),
245
+ )
246
+ try:
247
+ for layer, attn_fn in zip(self.model.decoder.layers, compiled):
248
+ mask = (
249
+ slide_mask
250
+ if layer.layer_type == "sliding_attention"
251
+ else full_mask
252
+ )
253
+ dummy = attn_fn(dummy, offset, mask)
254
+ mx.eval(dummy)
255
+ self._compiled_attn_layers = compiled
256
+ print("[mk1] attention compile ready", flush=True)
257
+ except ValueError as exc:
258
+ print(f"[mk1] attention compile fallback: {exc}", flush=True)
259
+ self._compiled_attn_layers = None
260
+
261
+ def decoder_logits(
262
+ self,
263
+ decoder_input_ids: mx.array,
264
+ latent_context: mx.array,
265
+ cache,
266
+ offset: mx.array,
267
+ full_mask: mx.array,
268
+ slide_mask: mx.array,
269
+ compiled_decoder_step=None,
270
+ ) -> tuple[mx.array, mx.array]:
271
+ del compiled_decoder_step
272
+ hidden_states = decoder_hidden_states(
273
+ self.model.decoder,
274
+ decoder_input_ids,
275
+ latent_context,
276
+ cache,
277
+ offset,
278
+ full_mask,
279
+ slide_mask,
280
+ compiled_attn_layers=self._compiled_attn_layers,
281
+ )
282
+ logits = self.model.decoder.embed_tokens.as_linear(hidden_states)
283
+ return _softcap(logits, self.final_logit_softcapping), hidden_states
284
+
285
+ def __call__(
286
+ self,
287
+ *,
288
+ decoder_input_ids: mx.array,
289
+ cache,
290
+ previous_confidence: mx.array | None = None,
291
+ previous_entropy: mx.array | None = None,
292
+ token_age: mx.array | None = None,
293
+ latent_state: LatentDeliberationState | None = None,
294
+ history_hidden_state: mx.array | None = None,
295
+ decoder_attention_mask: mx.array | None = None,
296
+ return_proposal_statistics: bool = False,
297
+ denoise_temperature: float | None = None,
298
+ repetition_token_mask: mx.array | None = None,
299
+ repetition_penalty: float = 1.0,
300
+ compiled_decoder_step=None,
301
+ profiler=None,
302
+ ) -> ModilifyMk1StepOutput:
303
+ """Run one inference step over a noisy diffusion canvas."""
304
+
305
+ del compiled_decoder_step
306
+ dtype = self.model.decoder.embed_tokens.weight.dtype
307
+ if profiler is not None:
308
+ span = profiler.measure("latent", decoder_input_ids)
309
+ latent_context, next_state = self._prepare_latent_context(
310
+ decoder_input_ids,
311
+ history_hidden_state=history_hidden_state,
312
+ confidence=previous_confidence,
313
+ entropy=previous_entropy,
314
+ age=token_age,
315
+ latent_state=latent_state,
316
+ dtype=dtype,
317
+ )
318
+ if profiler is not None:
319
+ span.done(latent_context, next_state.token_latents, next_state.memory_slots)
320
+ del decoder_attention_mask
321
+ from .fast_decode import _cache_capacity, build_decoder_masks
322
+
323
+ prefix_len = int(getattr(cache[0], "offset", 0))
324
+ offset = mx.array(prefix_len)
325
+ canvas_length = int(decoder_input_ids.shape[1])
326
+ full_mask, slide_mask = build_decoder_masks(
327
+ prefix_len=prefix_len,
328
+ canvas_length=canvas_length,
329
+ cache_capacity=max(_cache_capacity(cache), 1),
330
+ sliding_window=int(self.config.text_config.sliding_window),
331
+ batch_size=int(decoder_input_ids.shape[0]),
332
+ )
333
+ hidden_states = decoder_hidden_states(
334
+ self.model.decoder,
335
+ decoder_input_ids,
336
+ latent_context,
337
+ cache,
338
+ offset,
339
+ full_mask,
340
+ slide_mask,
341
+ compiled_attn_layers=None,
342
+ profiler=profiler,
343
+ )
344
+ if profiler is not None:
345
+ span = profiler.measure("lm_head", hidden_states)
346
+ logits = self.model.decoder.embed_tokens.as_linear(hidden_states)
347
+ logits = _softcap(logits, self.final_logit_softcapping)
348
+ if profiler is not None:
349
+ span.done(logits)
350
+ statistics = (None, None, None, None, None)
351
+ if return_proposal_statistics:
352
+ if profiler is not None:
353
+ span = profiler.measure("softmax", logits)
354
+ statistics = self._proposal_statistics(
355
+ logits,
356
+ denoise_temperature=denoise_temperature,
357
+ repetition_token_mask=repetition_token_mask,
358
+ repetition_penalty=repetition_penalty,
359
+ )
360
+ if profiler is not None:
361
+ span.done(
362
+ statistics[0],
363
+ statistics[1],
364
+ statistics[2],
365
+ statistics[3],
366
+ statistics[4],
367
+ )
368
+ return ModilifyMk1StepOutput(
369
+ logits=None if return_proposal_statistics else logits,
370
+ heavy_hidden_state=hidden_states,
371
+ next_latent_state=next_state,
372
+ cache=cache,
373
+ latent_context=latent_context,
374
+ proposal=statistics[0],
375
+ proposal_confidence=statistics[1],
376
+ token_entropy=statistics[2],
377
+ greedy_proposal=statistics[3],
378
+ greedy_confidence=statistics[4],
379
+ )
380
+
381
+
382
+ def _load_weight_files(model_path: Path) -> dict[str, mx.array]:
383
+ weight_files = sorted(model_path.glob("*.safetensors"))
384
+ if not weight_files:
385
+ raise FileNotFoundError(f"No safetensors found in {model_path}")
386
+ weights: dict[str, mx.array] = {}
387
+ for weight_file in weight_files:
388
+ weights.update(mx.load(str(weight_file)))
389
+ return weights
390
+
391
+
392
+ def load(
393
+ model_path: str | Path,
394
+ *,
395
+ lazy: bool = False,
396
+ expert_bits: int = 16,
397
+ expert_group_size: int = 64,
398
+ ):
399
+ """Load a native ``modilify_mk1`` MLX checkpoint."""
400
+
401
+ model_path = Path(model_path)
402
+ config = ModilifyMk1Config.from_json(model_path / "config.json")
403
+ if config.model_type != MODEL_TYPE:
404
+ raise ValueError(
405
+ f"Refusing to load model_type={config.model_type!r}; "
406
+ f"expected {MODEL_TYPE!r}."
407
+ )
408
+ print("[mk1] constructing graph", flush=True)
409
+ model = ModilifyMk1ForBlockDiffusion(config)
410
+ print("[mk1] reading shards", flush=True)
411
+ weights = _load_weight_files(model_path)
412
+ remapped = remap_state_dict(weights.items())
413
+ if len(remapped) != len(weights) or any(key not in remapped for key in weights):
414
+ print(
415
+ f"[mk1] remapped {len(weights)} source tensors -> {len(remapped)} MLX tensors",
416
+ flush=True,
417
+ )
418
+ del weights
419
+ print(f"[mk1] loading {len(remapped)} tensors", flush=True)
420
+ model.load_weights(list(remapped.items()), strict=True)
421
+ del remapped
422
+ if not lazy:
423
+ print("[mk1] evaluating parameters", flush=True)
424
+ mx.eval(model.parameters())
425
+ if expert_bits and expert_bits < 16:
426
+ quantize_experts(
427
+ model, bits=int(expert_bits), group_size=int(expert_group_size)
428
+ )
429
+ return model, config
430
+
431
+
432
+ def parameter_names(model: nn.Module) -> list[str]:
433
+ return [name for name, _ in tree_flatten(model.parameters())]
modilify_mlx/profile.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Modilify
2
+ # SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
3
+ """Optional per-denoise phase timers. Enabled only with --profile."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections import defaultdict
8
+ import time
9
+
10
+ import mlx.core as mx
11
+
12
+ PHASES = (
13
+ "latent",
14
+ "attn",
15
+ "moe",
16
+ "lm_head",
17
+ "softmax",
18
+ "commit",
19
+ "sync",
20
+ "update_cache",
21
+ )
22
+
23
+
24
+ class DenoiseProfiler:
25
+ def __init__(self) -> None:
26
+ self.totals = defaultdict(float)
27
+ self.steps = 0
28
+
29
+ def add(self, phase: str, seconds: float) -> None:
30
+ self.totals[phase] += float(seconds)
31
+
32
+ def finish_step(self) -> None:
33
+ self.steps += 1
34
+
35
+ def measure(self, phase: str, *arrays: mx.array):
36
+ mx.eval(*arrays)
37
+ started = time.perf_counter()
38
+
39
+ class _Span:
40
+ def __init__(self, profiler: DenoiseProfiler, name: str) -> None:
41
+ self.profiler = profiler
42
+ self.name = name
43
+ self.started = started
44
+
45
+ def done(self, *outputs: mx.array) -> None:
46
+ if outputs:
47
+ mx.eval(*outputs)
48
+ self.profiler.add(self.name, time.perf_counter() - self.started)
49
+
50
+ return _Span(self, phase)
51
+
52
+ def summary(self) -> str:
53
+ counted = sum(self.totals[name] for name in PHASES)
54
+ lines = [
55
+ f"[profile] steps={self.steps} accounted={counted:.3f}s",
56
+ ]
57
+ for name in PHASES:
58
+ value = self.totals[name]
59
+ share = (100.0 * value / counted) if counted else 0.0
60
+ per = (value / self.steps) if self.steps else 0.0
61
+ lines.append(
62
+ f"[profile] {name:12s} {value:7.3f}s {share:5.1f}% {per*1000:6.1f} ms/step"
63
+ )
64
+ return "\n".join(lines)