File size: 26,914 Bytes
2fb5bce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bbb15
 
 
 
 
2fb5bce
 
 
 
 
 
 
50bbb15
 
 
2fb5bce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bbb15
2fb5bce
50bbb15
2fb5bce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50bbb15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2fb5bce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
"""Fuse-2 model: Qwen3 host + DeepSeek V4 Flash coding experts.

Architecture: per-layer expert augmentation (Option B from the master plan).
At each augmented host layer, coding experts from DeepSeek V4 Flash are added
alongside the host's native FFN. A learned router decides which experts fire.

Key design principles (from fuse1 lessons):
  - bridge_out zero-init β†’ model starts as exact Qwen3-4B
  - repair_up zero-init β†’ no residual correction initially
  - Router initialized to low activation β†’ coding path fires rarely at first
  - Frozen experts, frozen host β†’ only bridges + routers + repair train
  - KV cache supported β€” host attention uses DynamicCache, expert path is
    per-token (no cross-token attention) so cache works transparently
"""
from __future__ import annotations

import math
from copy import deepcopy
from typing import Iterator

import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import Qwen3Config, Qwen3ForCausalLM


class Fuse2Config(Qwen3Config):
    """Qwen3 config extended with Fuse-2 MoE coding expert parameters."""

    model_type = "fuse2"

    def __init__(
        self,
        # Expert configuration
        expert_hidden_size: int = 4096,       # DeepSeek V4 hidden
        expert_intermediate_size: int = 2048,  # DeepSeek V4 expert intermediate
        experts_per_layer: dict | None = None, # layer_idx -> list of expert IDs
        num_augmented_layers: int = 0,
        top_k_experts: int = 2,
        # Bridge configuration
        bridge_rank: int = 7,
        coding_enabled: bool = True,
        # Router configuration
        router_init_scale: float = -2.0,  # low initial activation
        load_balance_coef: float = 0.01,
        **kwargs,
    ):
        super().__init__(**kwargs)
        self.expert_hidden_size = expert_hidden_size
        self.expert_intermediate_size = expert_intermediate_size
        self.experts_per_layer = experts_per_layer or {}
        self.num_augmented_layers = num_augmented_layers
        self.top_k_experts = top_k_experts
        self.bridge_rank = bridge_rank
        self.coding_enabled = coding_enabled
        self.router_init_scale = router_init_scale
        self.load_balance_coef = load_balance_coef
        self.architectures = ["Fuse2ForCausalLM"]


class SwiGLUExpert(nn.Module):
    """A single DeepSeek V4 Flash expert (SwiGLU FFN).

    gate_proj: (intermediate, hidden)
    up_proj:   (intermediate, hidden)
    down_proj: (hidden, intermediate)
    """

    # DeepSeek V4 Flash uses swiglu_limit=10.0 to clamp intermediate
    # activations. Without this, outlier values grow exponentially across
    # 36 layers and produce NaN by layer 6.
    SWIGLU_LIMIT = 10.0

    def __init__(self, hidden_size: int, intermediate_size: int):
        super().__init__()
        self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
        self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
        self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        gate_up = F.silu(self.gate_proj(x)) * self.up_proj(x)
        gate_up = gate_up.clamp(-self.SWIGLU_LIMIT, self.SWIGLU_LIMIT)
        return self.down_proj(gate_up)


class Fuse2Router(nn.Module):
    """Per-layer router for coding experts.

    Uses sqrtsoftplus scoring (matching DeepSeek V4's approach) with
    top-k selection and optional load balancing.
    """

    def __init__(
        self,
        input_dim: int,
        num_experts: int,
        top_k: int = 2,
        init_scale: float = -2.0,
    ):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = min(top_k, num_experts)
        self.gate = nn.Linear(input_dim, num_experts, bias=False)
        # Initialize to low activation so coding path fires rarely at start
        # Skip init on meta device (used by low_cpu_mem_usage / init_empty_weights)
        if self.gate.weight.device.type != 'meta':
            nn.init.normal_(self.gate.weight, mean=0.0, std=0.01)
        self.init_scale = init_scale

    def forward(
        self,
        hidden_states: torch.Tensor,
    ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
        """Route tokens to experts.

        Args:
            hidden_states: (batch*seq, expert_hidden) β€” already bridged

        Returns:
            router_weights: (batch*seq, top_k) β€” softmax weights for selected experts
            expert_indices: (batch*seq, top_k) β€” which experts were selected
            router_logits: (batch*seq, num_experts) β€” raw logits for load balancing
        """
        # sqrtsoftplus scoring (from DeepSeek V4)
        # Clamp softplus to min=1e-6 before sqrt to prevent NaN gradients
        logits = self.gate(hidden_states)  # (tokens, num_experts)
        scores = F.softplus(logits).clamp(min=1e-6).sqrt()

        # Top-k selection
        topk_weights, topk_indices = scores.topk(self.top_k, dim=-1)

        # Normalize weights
        topk_weights = topk_weights / (topk_weights.sum(dim=-1, keepdim=True) + 1e-8)

        return topk_weights, topk_indices, logits


class Fuse2AugmentedLayer(nn.Module):
    """One Qwen3 layer augmented with DeepSeek V4 coding experts.

    Forward flow (v2 β€” with residual-safe architecture):
      1. Standard Qwen3 attention + FFN (frozen)
      2. bridge_in: host_hidden -> expert_hidden
      3. router: select top-k coding experts
      4. experts: parallel SwiGLU computation
      5. expert_output normalized to match expert_input scale
      6. bridge_out: expert_hidden -> host_hidden
      7. coding_norm: RMSNorm on coding_delta (learned scale)
      8. coding_gate: sigmoid gate (learned, init ~0.12)
      9. residual clamp: bound delta to max 10% of residual norm
     10. repair: rank-r residual correction (zero-init)
     11. hidden += gated coding_delta + repair_delta

    v2 safeguards (prevent residual stream degeneration):
      - Expert output normalization (consistent magnitude)
      - RMSNorm on coding_delta (bounded scale)
      - Learnable sigmoid gate (controls contribution strength)
      - Residual-safe clamping (prevents any layer from overwhelming)
    """

    def __init__(
        self,
        host_layer: nn.Module,
        host_hidden: int,
        expert_hidden: int,
        expert_intermediate: int,
        num_experts: int,
        top_k: int = 2,
        bridge_rank: int = 7,
        router_init_scale: float = -2.0,
        coding_enabled: bool = True,
        # v2 safeguards
        max_delta_ratio: float = 0.1,  # coding_delta <= 10% of residual norm
    ):
        super().__init__()
        self.host_layer = host_layer
        self.coding_enabled = coding_enabled
        self.num_experts = num_experts
        self.top_k = top_k
        self.max_delta_ratio = max_delta_ratio

        # Expose host layer attributes needed by the Qwen3 model forward pass
        self.attention_type = getattr(host_layer, "attention_type", "full_attention")

        # Bridge: host space <-> expert space
        self.bridge_in = nn.Linear(host_hidden, expert_hidden, bias=False)
        self.bridge_out = nn.Linear(expert_hidden, host_hidden, bias=False)

        # Router
        self.router = Fuse2Router(
            expert_hidden, num_experts, top_k, router_init_scale
        )

        # Experts (frozen, loaded from DeepSeek V4 Flash)
        self.experts = nn.ModuleList([
            SwiGLUExpert(expert_hidden, expert_intermediate)
            for _ in range(num_experts)
        ])

        # v2: RMSNorm on coding_delta (learned scale, init=1.0)
        # This normalizes the coding contribution to unit variance before
        # the gate scales it down. The learned scale allows the model to
        # adjust per-layer contribution magnitude during training.
        self.coding_norm = nn.RMSNorm(host_hidden, eps=1e-6)

        # v2: Learnable sigmoid gate (init=-2.0 -> sigmoid(-2) ~ 0.12)
        # This lets the model learn how strongly to incorporate coding at
        # each layer. Starting at 12% ensures coding contributes but can't
        # dominate. The gate is a single scalar per layer.
        self.coding_gate = nn.Parameter(torch.tensor(-2.0))

        # Residual repair (low-rank)
        self.repair_down = nn.Linear(host_hidden, bridge_rank, bias=False)
        self.repair_up = nn.Linear(bridge_rank, host_hidden, bias=False)

        # Initialize for preservation: zero-init bridge_out and repair_up
        # Skip init on meta device (used by low_cpu_mem_usage / init_empty_weights)
        if self.bridge_in.weight.device.type != 'meta':
            nn.init.normal_(self.bridge_in.weight, mean=0.0, std=0.02)
            nn.init.zeros_(self.bridge_out.weight)
            nn.init.normal_(self.repair_down.weight, mean=0.0, std=0.02)
            nn.init.zeros_(self.repair_up.weight)

    def _expert_computation(self, expert_input: torch.Tensor) -> torch.Tensor:
        """Compute expert output from expert_input.

        This is the checkpointed part of the forward pass. It includes:
        - Router scoring and top-k selection
        - Expert SwiGLU computation (reads weights from disk via monkey-patched forward)
        - Weighted accumulation of expert outputs
        - v2: Expert output normalization (consistent magnitude)

        When wrapped in torch.utils.checkpoint, only expert_input is saved
        during forward. The entire computation (including disk reads for expert
        weights) is recomputed during backward, keeping VRAM bounded to one
        layer's worth of expert weights at a time.
        """
        # Route to experts
        topk_weights, expert_indices, router_logits = self.router(expert_input)

        # Compute expert outputs (sparse β€” only selected experts)
        # Use index_add for autograd-safe accumulation (no in-place ops).
        expert_output = torch.zeros_like(expert_input)

        # Fast path: single-token generation (no loop over all experts)
        # During generation, expert_input is (1, expert_hidden).
        # expert_indices is (1, top_k). We can directly call the 2 selected
        # experts without iterating over all num_experts checking masks.
        if expert_input.shape[0] == 1 and not self.training:
            for k in range(self.top_k):
                eid = expert_indices[0, k].item()
                expert_out = self.experts[eid](expert_input)
                expert_output = expert_output + topk_weights[0, k].unsqueeze(-1) * expert_out
        else:
            # Multi-token path: group tokens by expert (original logic)
            for k in range(self.top_k):
                indices = expert_indices[:, k]  # (tokens,)
                weights = topk_weights[:, k]    # (tokens,)

                # Group tokens by expert for efficient computation
                for eid in range(self.num_experts):
                    mask = indices == eid
                    if not mask.any():
                        continue
                    expert_in = expert_input[mask]
                    expert_out = self.experts[eid](expert_in)
                    weighted = weights[mask].unsqueeze(-1) * expert_out
                    token_idx = torch.where(mask)[0]
                    expert_output = expert_output.index_add(
                        0, token_idx, weighted.to(expert_output.dtype))

        # v2: Expert output normalization
        # Normalize expert_output to match expert_input's per-token norm.
        # This ensures consistent output magnitude regardless of which experts
        # were selected or their internal scale differences. Without this,
        # different expert combinations produce wildly different output scales,
        # causing inconsistent coding_delta magnitudes that corrupt the residual.
        #
        # IMPORTANT: Detach in_norm from autograd to prevent the normalization
        # from canceling bridge_in's gradient. Without detach, the gradient
        # through in_norm (which depends on expert_input = bridge_in(h)) partially
        # cancels the gradient through the experts, killing bridge_in learning.
        out_norm = expert_output.norm(dim=-1, keepdim=True) + 1e-6
        in_norm = expert_input.norm(dim=-1, keepdim=True).detach() + 1e-6
        expert_output = expert_output * (in_norm / out_norm)

        return expert_output

    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: torch.Tensor | None = None,
        position_ids: torch.LongTensor | None = None,
        past_key_values=None,
        use_cache: bool | None = False,
        position_embeddings=None,
        **kwargs,
    ) -> torch.Tensor:
        # 1. Run the host layer (attention + FFN)
        hidden_states = self.host_layer(
            hidden_states,
            attention_mask=attention_mask,
            position_ids=position_ids,
            past_key_values=past_key_values,
            use_cache=use_cache,
            position_embeddings=position_embeddings,
            **kwargs,
        )

        if not self.coding_enabled or self.num_experts == 0:
            return hidden_states

        # v2-phase: Check for per-token coding mask
        # If coding_mask is set on the model, only apply coding to tokens
        # where mask=True. This enables phase-based coding:
        #   - Reasoning tokens (before </thinking>): coding OFF (pure Qwen)
        #   - Answer tokens (after </thinking>): coding ON (DeepSeek contributes)
        coding_mask = getattr(self, '_coding_mask', None)

        original_shape = hidden_states.shape
        h_flat = hidden_states.reshape(-1, original_shape[-1])

        # If we have a coding_mask, select only the tokens that need coding
        if coding_mask is not None:
            # coding_mask shape: (batch, seq_len) -> (tokens,)
            mask_flat = coding_mask.reshape(-1)
            if not mask_flat.any():
                # No tokens need coding β€” skip entirely
                return hidden_states
            # Only process tokens where mask is True
            h_coding = h_flat[mask_flat]  # (n_coding_tokens, hidden)
        else:
            mask_flat = None
            h_coding = h_flat  # Process all tokens

        # 2. Bridge to expert space
        expert_input = self.bridge_in(h_coding)  # (n_tokens, expert_hidden)

        # 3-4. Expert computation (router + experts + accumulation)
        # Use gradient checkpointing during training to avoid storing all
        # expert weights in VRAM. The checkpoint saves only expert_input
        # during forward and recomputes the expert computation (re-reading
        # weights from disk) during backward. This keeps peak VRAM bounded
        # to one layer's worth of expert weights at a time.
        if self.training and expert_input.requires_grad:
            expert_output = torch.utils.checkpoint.checkpoint(
                self._expert_computation, expert_input, use_reentrant=False,
            )
        else:
            expert_output = self._expert_computation(expert_input)

        # 5. Bridge back to host space
        coding_delta = self.bridge_out(expert_output)

        # v2-6a: RMSNorm on coding_delta
        coding_delta = self.coding_norm(coding_delta)

        # v2-6b: Learnable sigmoid gate
        gate = torch.sigmoid(self.coding_gate)
        coding_delta = coding_delta * gate

        # v2-6c: Residual-safe clamping
        h_for_norm = h_coding if coding_mask is not None else h_flat
        h_norm = h_for_norm.norm(dim=-1, keepdim=True) + 1e-6
        delta_norm = coding_delta.norm(dim=-1, keepdim=True) + 1e-6
        max_delta = h_norm * self.max_delta_ratio
        scale = (max_delta / delta_norm).clamp(max=1.0)
        coding_delta = coding_delta * scale

        # 7. Repair (only for coding tokens)
        repair_delta = self.repair_up(self.repair_down(h_for_norm))

        # 8. Residual addition
        # If using a mask, scatter coding_delta back to the right positions
        if coding_mask is not None:
            result = h_flat.clone()
            result[mask_flat] = h_coding + coding_delta + repair_delta
            return result.reshape(original_shape)
        else:
            result = h_flat + coding_delta + repair_delta
            return result.reshape(original_shape)

    def get_router_logits(self) -> torch.Tensor | None:
        """Return last router logits for load balancing loss."""
        return getattr(self, "_last_router_logits", None)


class Fuse2ForCausalLM(Qwen3ForCausalLM):
    """Qwen3-4B host + DeepSeek V4 Flash coding experts.

    The model starts as an exact Qwen3-4B (zero-init bridges) and learns
    to incorporate coding experts through bridge and router training.
    """

    config_class = Fuse2Config
    _no_split_modules = ["Qwen3DecoderLayer", "Fuse2AugmentedLayer"]

    def __init__(self, config: Fuse2Config):
        super().__init__(config)

        # Replace specified layers with augmented versions
        experts_per_layer = config.experts_per_layer or {}
        augmented_count = 0

        for layer_idx_str, expert_ids in experts_per_layer.items():
            layer_idx = int(layer_idx_str)
            if layer_idx >= len(self.model.layers):
                raise ValueError(
                    f"Layer {layer_idx} out of range "
                    f"(model has {len(self.model.layers)} layers)"
                )

            num_experts = len(expert_ids)
            if num_experts == 0:
                continue

            original_layer = self.model.layers[layer_idx]
            self.model.layers[layer_idx] = Fuse2AugmentedLayer(
                host_layer=original_layer,
                host_hidden=config.hidden_size,
                expert_hidden=config.expert_hidden_size,
                expert_intermediate=config.expert_intermediate_size,
                num_experts=num_experts,
                top_k=min(config.top_k_experts, num_experts),
                bridge_rank=config.bridge_rank,
                router_init_scale=config.router_init_scale,
                coding_enabled=config.coding_enabled,
            )
            augmented_count += 1

        config.num_augmented_layers = augmented_count

    def set_coding_enabled(self, enabled: bool) -> None:
        """Toggle the coding expert path."""
        for layer in self.model.layers:
            if isinstance(layer, Fuse2AugmentedLayer):
                layer.coding_enabled = enabled

    def set_coding_mask(self, mask: torch.Tensor | None) -> None:
        """Set per-token coding mask for phase-based coding.

        When set, only tokens where mask=True will have the coding path
        applied. Tokens where mask=False get pure host (Qwen3) output.

        This enables the "Qwen reasons, DeepSeek codes" architecture:
        - Set mask=False for reasoning tokens (before </thinking>)
        - Set mask=True for answer tokens (after </thinking>)

        Pass None to disable masking (coding applies to all tokens).
        """
        for layer in self.model.layers:
            if isinstance(layer, Fuse2AugmentedLayer):
                layer._coding_mask = mask

    def get_augmented_layers(self) -> list[tuple[int, Fuse2AugmentedLayer]]:
        """Return (index, layer) pairs for all augmented layers."""
        return [
            (i, layer)
            for i, layer in enumerate(self.model.layers)
            if isinstance(layer, Fuse2AugmentedLayer)
        ]

    def get_trainable_params(self) -> dict[str, nn.Parameter]:
        """Return only the trainable parameters (bridges, routers, repair, v2 safeguards)."""
        trainable = {}
        for name, param in self.named_parameters():
            if any(
                key in name
                for key in ("bridge_in", "bridge_out", "router",
                           "repair_down", "repair_up",
                           "coding_norm", "coding_gate")
            ):
                trainable[name] = param
        return trainable

    def freeze_host_and_experts(self) -> None:
        """Freeze everything except bridges, routers, repair, and v2 safeguards."""
        for name, param in self.named_parameters():
            if any(
                key in name
                for key in ("bridge_in", "bridge_out", "router",
                           "repair_down", "repair_up",
                           "coding_norm", "coding_gate")
            ):
                param.requires_grad = True
            else:
                param.requires_grad = False

    def count_parameters(self) -> dict[str, int]:
        """Count parameters by category."""
        counts = {
            "host": 0,
            "experts": 0,
            "bridges": 0,
            "routers": 0,
            "repair": 0,
            "total": 0,
            "trainable": 0,
        }
        for name, param in self.named_parameters():
            n = param.numel()
            counts["total"] += n
            if param.requires_grad:
                counts["trainable"] += n

            if "bridge_in" in name or "bridge_out" in name:
                counts["bridges"] += n
            elif "router" in name:
                counts["routers"] += n
            elif "repair" in name:
                counts["repair"] += n
            elif "experts" in name:
                counts["experts"] += n
            else:
                counts["host"] += n

        return counts

    def forward(
        self,
        input_ids: torch.LongTensor | None = None,
        attention_mask: torch.Tensor | None = None,
        position_ids: torch.LongTensor | None = None,
        past_key_values=None,
        inputs_embeds: torch.FloatTensor | None = None,
        labels: torch.LongTensor | None = None,
        use_cache: bool | None = None,
        **kwargs,
    ):
        return super().forward(
            input_ids=input_ids,
            attention_mask=attention_mask,
            position_ids=position_ids,
            past_key_values=past_key_values,
            inputs_embeds=inputs_embeds,
            labels=labels,
            use_cache=use_cache,
            **kwargs,
        )

    @classmethod
    def from_pretrained(cls, *args, **kwargs):
        """Load from HuggingFace Hub with automatic runtime fixes.

        This overrides the default from_pretrained to apply three critical
        fixes after weight loading:

        1. Initialize coding_gate and coding_norm if they're still on meta
           device (these params are not in the safetensors checkpoint).
        2. Cast all RMSNorm/LayerNorm weights from float32 to bfloat16 to
           enable fused SDPA kernel dispatch (otherwise falls back to slow
           Python loops).
        3. Ensure coding_enabled is True (config default).

        With these fixes, from_pretrained produces a working model without
        any manual post-load patching.
        """
        model = super().from_pretrained(*args, **kwargs)
        model._apply_runtime_fixes()
        return model

    def _apply_runtime_fixes(self):
        """Apply runtime fixes after weight loading.

        Called automatically by from_pretrained. Can also be called manually
        if the model was loaded via a custom path (e.g., init_empty_weights +
        manual safetensors loading).
        """
        device = next(self.parameters()).device
        fixed_meta = 0
        fixed_norms = 0

        for layer in self.model.layers:
            if not isinstance(layer, Fuse2AugmentedLayer):
                continue

            # Fix 1: coding_gate on meta device β†’ initialize to -2.0
            if hasattr(layer, 'coding_gate'):
                if layer.coding_gate.device.type == 'meta':
                    layer.coding_gate = nn.Parameter(
                        torch.tensor(-2.0, device=device))
                    fixed_meta += 1

            # Fix 2: coding_norm on meta device β†’ create fresh RMSNorm
            if hasattr(layer, 'coding_norm'):
                if hasattr(layer.coding_norm, 'weight') and \
                   layer.coding_norm.weight.device.type == 'meta':
                    layer.coding_norm = nn.RMSNorm(
                        layer.coding_norm.weight.shape[0], eps=1e-6).to(device)
                    fixed_meta += 1

        # Fix 3: Cast float32 norm weights to bfloat16 for fused kernels
        for module in self.modules():
            if hasattr(module, 'weight') and hasattr(module, 'eps'):
                if module.weight.dtype == torch.float32:
                    module.weight.data = module.weight.data.to(torch.bfloat16)
                    fixed_norms += 1

        # Ensure coding is enabled
        self.set_coding_enabled(True)

        return {"meta_params_fixed": fixed_meta, "norms_cast_to_bf16": fixed_norms}


def load_expert_weights(
    model: Fuse2ForCausalLM,
    expert_dir: str,
    expert_mapping: dict[int, list[int]],
) -> dict:
    """Load extracted DeepSeek V4 expert weights into the Fuse2 model.

    Args:
        model: Fuse2 model with augmented layers
        expert_dir: directory containing expert safetensors
        expert_mapping: layer_idx -> list of expert IDs (matching selection order)

    Returns:
        Manifest of loaded tensors with hash verification
    """
    from safetensors.torch import load_file
    import glob

    # Load all shards
    shard_files = sorted(glob.glob(f"{expert_dir}/experts-*.safetensors"))
    if not shard_files:
        raise FileNotFoundError(f"No expert shards found in {expert_dir}")

    all_tensors = {}
    for shard in shard_files:
        all_tensors.update(load_file(shard))

    loaded = {}
    for layer_idx, expert_ids in expert_mapping.items():
        augmented = model.model.layers[layer_idx]
        if not isinstance(augmented, Fuse2AugmentedLayer):
            raise ValueError(f"Layer {layer_idx} is not augmented")

        for local_idx, global_eid in enumerate(expert_ids):
            prefix = f"layer{layer_idx:02d}_expert{global_eid:03d}"

            for pname in ("gate_proj.weight", "up_proj.weight", "down_proj.weight"):
                key = f"{prefix}.{pname}"
                if key not in all_tensors:
                    raise KeyError(f"Missing expert tensor: {key}")

                tensor = all_tensors[key]
                target_name = pname.replace(".", "_").replace("_weight", "")
                # Map to expert module
                parts = pname.split(".")
                module = augmented.experts[local_idx]
                for part in parts[:-1]:
                    module = getattr(module, part)
                param = getattr(module, parts[-1])
                param.data.copy_(tensor.to(param.dtype))

                loaded[key] = {
                    "shape": list(tensor.shape),
                    "destination": f"layers.{layer_idx}.experts.{local_idx}.{pname}",
                }

    return loaded