File size: 16,233 Bytes
7ae0b4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
"""Text-only loader for the Bonsai 2 MLX pack.

This keeps the language model numerically identical to the pack's bundled loader,
but does not instantiate or evaluate the 0.92 GB vision tower for text-only use.
"""

from __future__ import annotations

import json
import math
import os
import sys
from pathlib import Path

import mlx.core as mx
import mlx.nn as nn
from mlx_lm.models.qwen3_5 import TextModel, TextModelArgs
from mlx_lm.models.activations import swiglu
from mlx_lm.models.base import scaled_dot_product_attention


def _touch_placeholder_cache(cache):
    """Give pruned-layer ArraysCache entries serializable, inert state."""
    if cache is not None and cache[0] is None:
        cache[0] = mx.zeros((1,), dtype=mx.uint8)
        cache[1] = mx.zeros((1,), dtype=mx.uint8)


class SkippedDecoderLayer(nn.Module):
    """Identity replacement used only by the explicitly enabled Turbo mode."""

    def __init__(self, is_linear, scale=1.0):
        super().__init__()
        self.original_is_linear = is_linear
        # Identity layers need only a harmless cache placeholder. ArraysCache
        # tolerates an empty state; KVCache.state assumes allocated keys.
        self.is_linear = True
        self.scale = float(scale)

    def __call__(self, x, mask=None, cache=None):
        _touch_placeholder_cache(cache)
        return x if self.scale == 1.0 else x * self.scale


class AffineSkippedDecoderLayer(nn.Module):
    """Cheap calibrated diagonal approximation for an omitted decoder block."""

    def __init__(self, is_linear, scale, bias):
        super().__init__()
        self.original_is_linear = is_linear
        self.is_linear = True
        self.scale = scale
        self.bias = bias

    def __call__(self, x, mask=None, cache=None):
        _touch_placeholder_cache(cache)
        return x * self.scale + self.bias


class SkippedMLP(nn.Module):
    """Zero residual used to omit only a decoder block's MLP branch."""

    def __call__(self, x):
        return mx.zeros_like(x)


class AffineSkippedMLP(nn.Module):
    """Calibrated diagonal approximation of an MLP residual branch."""

    def __init__(self, scale, bias):
        super().__init__()
        self.scale = scale
        self.bias = bias

    def __call__(self, x):
        return x * self.scale + self.bias


class FusedDecodeProjection(nn.Module):
    """Fuse sibling ternary GEMVs only when autoregressive decode benefits."""

    def __init__(self, projections, fwht):
        super().__init__()
        first = projections[0]
        if any(
            projection.block != first.block or projection.signs is not first.signs
            for projection in projections[1:]
        ):
            raise ValueError("Cannot fuse projections with different transforms")
        self.weight = mx.concatenate([p.weight for p in projections], axis=0)
        self.scales = mx.concatenate([p.scales for p in projections], axis=0)
        self.biases = mx.concatenate([p.biases for p in projections], axis=0)
        self.signs = first.signs
        self.block = first.block
        sizes = [projection.weight.shape[0] for projection in projections]
        self.boundaries = [sum(sizes[: index + 1]) for index in range(len(sizes) - 1)]
        self.slices = []
        start = 0
        for size in sizes:
            self.slices.append(slice(start, start + size))
            start += size
        self._fwht = fwht

    def _matmul(self, x, weight, scales, biases):
        return mx.quantized_matmul(
            x,
            weight,
            scales,
            biases,
            transpose=True,
            group_size=128,
            bits=2,
        )

    def __call__(self, x):
        x = self._fwht(x, self.block, self.signs)
        # One wide GEMV is faster during autoregressive decode. For prompt
        # batches, two narrower QMMs are slightly faster on Apple M4.
        if math.prod(x.shape[:-1]) == 1:
            combined = self._matmul(x, self.weight, self.scales, self.biases)
            return tuple(mx.split(combined, self.boundaries, axis=-1))
        else:
            return tuple(
                self._matmul(
                    x,
                    self.weight[section],
                    self.scales[section],
                    self.biases[section],
                )
                for section in self.slices
            )


class FusedDecodeMLP(nn.Module):
    def __init__(self, projection, down):
        super().__init__()
        self.projection = projection
        self.down_proj = down

    def __call__(self, x):
        gate, up = self.projection(x)
        return self.down_proj(swiglu(gate, up))


class FusedDecodeAttention(nn.Module):
    def __init__(self, attention, projection):
        super().__init__()
        self.projection = projection
        self.num_key_value_heads = attention.num_key_value_heads
        self.num_attention_heads = attention.num_attention_heads
        self.head_dim = attention.head_dim
        self.scale = attention.scale
        self.q_norm = attention.q_norm
        self.k_norm = attention.k_norm
        self.rope = attention.rope
        self.o_proj = attention.o_proj

    def __call__(self, x, mask=None, cache=None):
        batch, length, _ = x.shape
        q_proj_output, keys, values = self.projection(x)
        queries, gate = mx.split(
            q_proj_output.reshape(batch, length, self.num_attention_heads, -1),
            2,
            axis=-1,
        )
        gate = gate.reshape(batch, length, -1)
        queries = self.q_norm(queries).transpose(0, 2, 1, 3)
        keys = self.k_norm(
            keys.reshape(batch, length, self.num_key_value_heads, -1)
        ).transpose(0, 2, 1, 3)
        values = values.reshape(
            batch, length, self.num_key_value_heads, -1
        ).transpose(0, 2, 1, 3)

        if cache is not None:
            queries = self.rope(queries, offset=cache.offset)
            keys = self.rope(keys, offset=cache.offset)
            keys, values = cache.update_and_fetch(keys, values)
        else:
            queries = self.rope(queries)
            keys = self.rope(keys)
        output = scaled_dot_product_attention(
            queries, keys, values, cache=cache, scale=self.scale, mask=mask
        )
        output = output.transpose(0, 2, 1, 3).reshape(batch, length, -1)
        return self.o_proj(output * mx.sigmoid(gate))


def load_text_model(directory: str | Path):
    directory = Path(directory)
    config = json.loads((directory / "config.json").read_text())
    if config.get("schema_version") != 2:
        raise ValueError(f"Unsupported schema version: {config.get('schema_version')!r}")
    if config.get("model_type") != "prism_hadamard_qwen35":
        raise ValueError(f"Unsupported model type: {config.get('model_type')!r}")

    runtime = directory / "runtime"
    sys.path.insert(0, str(runtime))
    from runtime import Packed, fwht

    model = TextModel(TextModelArgs.from_dict(config["text_config"]))
    all_weights = mx.load(str(directory / "model.safetensors"))
    prefix = "language_model."
    layout = config.get("optimized_weight_layout", {})
    share_signs = layout.get("sign_sharing") == "by_input_width"
    Packed.share_fwht_activations = bool(layout.get("shared_fwht_activations", False))

    seen = set()
    for record in config["modules"]:
        path = record["path"]
        if path in seen:
            raise ValueError(f"Duplicate packed module: {path}")
        seen.add(path)
        if record["dtype"] != "float16":
            raise ValueError(f"Unsupported activation dtype at {path}")

        parts = path.split(".")
        parent = model
        for part in parts[:-1]:
            parent = parent[int(part)] if part.isdigit() else getattr(parent, part)

        key = prefix + path
        arrays = [all_weights[key + "." + suffix] for suffix in ("weight", "scales", "biases")]
        block = record["block"]
        if block and block not in (512, 1024, 2048, 4096):
            raise ValueError(f"Unsupported Hadamard block size at {path}: {block}")
        signs = all_weights.get(key + ".signs")
        if block and signs is None:
            raise ValueError(f"Missing Hadamard signs at {path}")
        if signs is not None and not mx.all((signs == 1) | (signs == -1)).item():
            raise ValueError(f"Invalid Hadamard signs at {path}")
        setattr(
            parent,
            parts[-1],
            Packed(arrays, block, signs, record["embedding"], mx.float16),
        )

    text_weights = [
        (name[len(prefix) :], value)
        for name, value in all_weights.items()
        if name.startswith(prefix)
    ]
    model.load_weights(text_weights, strict=True)
    del text_weights

    # The checkpoint contains 402 sign tensors but only three unique, byte-identical
    # payloads (one for each input width). Point modules with the same width at one
    # canonical array after load_weights has assigned the checkpoint parameters.
    # This is an exact representation change: ternary weights, scales, biases, and
    # every sign value remain untouched.
    if share_signs:
        canonical_signs = {}
        for record in config["modules"]:
            if not record["block"]:
                continue
            parent = model
            parts = record["path"].split(".")
            for part in parts:
                parent = parent[int(part)] if part.isdigit() else getattr(parent, part)
            signs = parent.signs
            key = (tuple(signs.shape), signs.dtype)
            canonical = canonical_signs.get(key)
            if canonical is None:
                canonical_signs[key] = signs
            else:
                parent.signs = canonical

        expected = layout.get("unique_sign_payloads")
        if expected is not None and len(canonical_signs) != expected:
            raise ValueError(
                f"Expected {expected} canonical sign tensors, found {len(canonical_signs)}"
            )

    fuse_decode = os.environ.get("BONSAI_FUSE_DECODE") == "1" or bool(
        layout.get("fused_decode_projections", False)
    )
    if fuse_decode:
        for layer_index, layer in enumerate(model.model.layers):
            old_mlp = layer.mlp
            gate, up = old_mlp.gate_proj, old_mlp.up_proj
            projection = FusedDecodeProjection((gate, up), fwht)
            fused = FusedDecodeMLP(projection, old_mlp.down_proj)
            # Materialize one layer at a time and release its old gate/up arrays,
            # keeping peak memory bounded while the checkpoint is memory-mapped.
            mx.eval(projection.weight, projection.scales, projection.biases)
            layer.mlp = fused
            stem = f"{prefix}model.layers.{layer_index}.mlp."
            for projection in ("gate_proj", "up_proj"):
                for suffix in ("weight", "scales", "biases", "signs"):
                    all_weights.pop(stem + projection + "." + suffix, None)

            if not layer.is_linear:
                old_attention = layer.self_attn
                qkv = FusedDecodeProjection(
                    (
                        old_attention.q_proj,
                        old_attention.k_proj,
                        old_attention.v_proj,
                    ),
                    fwht,
                )
                mx.eval(qkv.weight, qkv.scales, qkv.biases)
                layer.self_attn = FusedDecodeAttention(old_attention, qkv)
                stem = f"{prefix}model.layers.{layer_index}.self_attn."
                for projection_name in ("q_proj", "k_proj", "v_proj"):
                    for suffix in ("weight", "scales", "biases", "signs"):
                        all_weights.pop(
                            stem + projection_name + "." + suffix, None
                        )

    profile_name = os.environ.get("BONSAI_PROFILE", "").strip()
    profile = {}
    if profile_name:
        profiles = config.get("optimized_runtime_profiles", {})
        if profile_name not in profiles:
            raise ValueError(f"Unknown BONSAI_PROFILE value: {profile_name!r}")
        profile = profiles[profile_name]
        profile_layers = profile.get("skip_layers", [])
        skip_text = ",".join(str(index) for index in profile_layers)
        config["active_runtime_profile"] = profile_name
    else:
        skip_text = os.environ.get("BONSAI_SKIP_LAYERS", "").strip()
    if skip_text:
        skip_layers = sorted({int(value) for value in skip_text.split(",")})
        adapter_path = os.environ.get("BONSAI_SKIP_ADAPTERS", "").strip()
        if not adapter_path and profile.get("skip_adapters"):
            adapter_path = str(directory / profile["skip_adapters"])
        skip_adapters = mx.load(adapter_path) if adapter_path else {}
        scale_text = os.environ.get("BONSAI_SKIP_SCALES", "").strip()
        skip_scales = {}
        if scale_text:
            for item in scale_text.split(","):
                index_text, scale_value = item.split(":", 1)
                skip_scales[int(index_text)] = float(scale_value)
        if any(index < 0 or index >= len(model.model.layers) for index in skip_layers):
            raise ValueError(f"Invalid BONSAI_SKIP_LAYERS value: {skip_text!r}")
        # Layers 0 and 3 own the reference SSM/attention caches used to build
        # masks for the whole stack and therefore cannot be identity-pruned.
        if {0, model.model.fa_idx} & set(skip_layers):
            raise ValueError("Turbo mode cannot skip the reference cache layers")
        for index in skip_layers:
            old_layer = model.model.layers[index]
            scale_key = f"layers.{index}.scale"
            bias_key = f"layers.{index}.bias"
            if adapter_path:
                if scale_key not in skip_adapters or bias_key not in skip_adapters:
                    raise ValueError(f"Missing affine adapter for skipped layer {index}")
                model.model.layers[index] = AffineSkippedDecoderLayer(
                    old_layer.is_linear,
                    skip_adapters[scale_key],
                    skip_adapters[bias_key],
                )
            else:
                model.model.layers[index] = SkippedDecoderLayer(
                    old_layer.is_linear, skip_scales.get(index, 1.0)
                )
        config["active_runtime_skip_layers"] = skip_layers
        if adapter_path:
            config["active_runtime_skip_adapters"] = adapter_path
        if skip_scales:
            config["active_runtime_skip_scales"] = skip_scales

    skip_mlp_text = os.environ.get("BONSAI_SKIP_MLP_LAYERS", "").strip()
    if not skip_mlp_text and profile.get("skip_mlp_layers"):
        skip_mlp_text = ",".join(str(index) for index in profile["skip_mlp_layers"])
    if skip_mlp_text:
        skip_mlp_layers = sorted({int(value) for value in skip_mlp_text.split(",")})
        mlp_adapter_path = os.environ.get("BONSAI_SKIP_MLP_ADAPTERS", "").strip()
        mlp_adapters = mx.load(mlp_adapter_path) if mlp_adapter_path else {}
        if any(index < 0 or index >= len(model.model.layers) for index in skip_mlp_layers):
            raise ValueError(f"Invalid BONSAI_SKIP_MLP_LAYERS value: {skip_mlp_text!r}")
        active_full_skips = set(config.get("active_runtime_skip_layers", []))
        if active_full_skips & set(skip_mlp_layers):
            raise ValueError("A layer cannot be both fully skipped and MLP-skipped")
        for index in skip_mlp_layers:
            scale_key = f"mlp_layers.{index}.scale"
            bias_key = f"mlp_layers.{index}.bias"
            if mlp_adapter_path:
                if scale_key not in mlp_adapters or bias_key not in mlp_adapters:
                    raise ValueError(f"Missing affine MLP adapter for layer {index}")
                model.model.layers[index].mlp = AffineSkippedMLP(
                    mlp_adapters[scale_key], mlp_adapters[bias_key]
                )
            else:
                model.model.layers[index].mlp = SkippedMLP()
        config["active_runtime_skip_mlp_layers"] = skip_mlp_layers
        if mlp_adapter_path:
            config["active_runtime_skip_mlp_adapters"] = mlp_adapter_path

    model.eval()
    mx.eval(model.parameters())
    return model, config