Image-Text-to-Text
MLX
Safetensors
modilify_mk1
diffusion
multimodal
mixture-of-experts
conversational
Instructions to use modilify/Modilify-Mk1-MLX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use modilify/Modilify-Mk1-MLX with MLX:
# Make sure mlx-vlm is installed # pip install --upgrade mlx-vlm from mlx_vlm import load, generate from mlx_vlm.prompt_utils import apply_chat_template from mlx_vlm.utils import load_config # Load the model model, processor = load("modilify/Modilify-Mk1-MLX") config = load_config("modilify/Modilify-Mk1-MLX") # Prepare input image = ["http://images.cocodataset.org/val2017/000000039769.jpg"] prompt = "Describe this image." # Apply chat template formatted_prompt = apply_chat_template( processor, config, prompt, num_images=1 ) # Generate output output = generate(model, processor, formatted_prompt, image) print(output) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Pi
How to use modilify/Modilify-Mk1-MLX with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "modilify/Modilify-Mk1-MLX"
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "modilify/Modilify-Mk1-MLX" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent
How to use modilify/Modilify-Mk1-MLX with Hermes Agent:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "modilify/Modilify-Mk1-MLX"
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default modilify/Modilify-Mk1-MLX
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use modilify/Modilify-Mk1-MLX with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "modilify/Modilify-Mk1-MLX"
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "modilify/Modilify-Mk1-MLX" \ --custom-provider-id mlx-lm \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Publish Modilify Mk1 MLX runtime
Browse files- README.md +59 -0
- config.json +149 -0
- convert_modilify_mk1_mlx.py +195 -0
- generate_modilify.py +116 -0
- modilify_mlx/__init__.py +12 -0
- modilify_mlx/commit_policy.py +273 -0
- modilify_mlx/config.py +165 -0
- modilify_mlx/convert_utils.py +124 -0
- modilify_mlx/fast_decode.py +298 -0
- modilify_mlx/generate.py +388 -0
- modilify_mlx/language.py +110 -0
- modilify_mlx/latent_deliberation.py +449 -0
- modilify_mlx/modeling.py +433 -0
- modilify_mlx/profile.py +64 -0
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 |
+

|
| 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)
|