Add KV-BSS (Key-Value Binding Softmax Sharpening) attention hook

#11
by F-Labs - opened
HADAMARD_QUANT_INTEGRATION.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MiniCPM5-2B-Hadamard-GSQ: KV-BSS integration
2
+
3
+ This pull request contains a self-contained implementation of the custom
4
+ MiniCPM Hadamard model path and the KV-BSS attention hook.
5
+
6
+ ## Included source files
7
+
8
+ 1. `configuration_minicpm_hadamard.py` defines the model and quantization
9
+ parameters, including the KV-BSS controls.
10
+ 2. `modeling_minicpm_hadamard.py` implements the model, causal masking, RoPE,
11
+ grouped-query attention, and legacy tuple cache support.
12
+ 3. `kv_bss.py` implements focus scaling and haze-floor filtering with explicit
13
+ shape validation and finite handling for fully masked rows.
14
+ 4. `test_inference.py` exercises a finite forward pass, GQA validation,
15
+ 2D/4D attention-mask behavior, and cached-versus-uncached logit parity.
16
+
17
+ ## Verification
18
+
19
+ Run from the model-code directory:
20
+
21
+ ```bash
22
+ python -m unittest discover -s . -p 'test_inference.py' -v
23
+ ```
24
+
25
+ The test is intentionally small and CPU-only. It validates implementation
26
+ behavior without downloading a checkpoint and does not claim benchmark
27
+ accuracy or long-context quality.
28
+
29
+ ## Scope
30
+
31
+ The PR contains code and tests only. The separately published quantized
32
+ checkpoint and its calibration report are linked from the model card:
33
+
34
+ https://huggingface.co/F-Labs/MiniCPM5-2B-Hadamard-GSQ
configuration_minicpm_hadamard.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MiniCPMHadamardConfig — explicit bifurcation_rank added."""
2
+ from transformers.configuration_utils import PretrainedConfig
3
+
4
+
5
+ class MiniCPMHadamardConfig(PretrainedConfig):
6
+ model_type = "minicpm_hadamard"
7
+ keys_to_ignore_at_inference = ["past_key_values"]
8
+
9
+ def __init__(
10
+ self,
11
+ vocab_size=130560,
12
+ hidden_size=2048,
13
+ intermediate_size=6144,
14
+ num_hidden_layers=42,
15
+ num_attention_heads=16,
16
+ num_key_value_heads=2,
17
+ head_dim=128,
18
+ hidden_act="silu",
19
+ max_position_embeddings=131072,
20
+ initializer_range=0.02,
21
+ rms_norm_eps=1e-6,
22
+ use_cache=True,
23
+ pad_token_id=1,
24
+ bos_token_id=0,
25
+ eos_token_id=[1, 130073],
26
+ tie_word_embeddings=False,
27
+ rope_theta=5000000.0,
28
+ bits=4,
29
+ group_size=64,
30
+ hadamard_block_size=128,
31
+ residual_rank=16,
32
+ bifurcation_rank=24,
33
+ k_proj_rank=32,
34
+ layer_rank_map=None,
35
+ dense_tensor_names=None,
36
+ int8_tensor_names=None,
37
+ rotation_mode="fixed_hadamard",
38
+ rotation_seed=1729,
39
+ tau_focus=1.10,
40
+ haze_floor_margin=12.0,
41
+ **kwargs,
42
+ ):
43
+ super().__init__(
44
+ pad_token_id=pad_token_id,
45
+ bos_token_id=bos_token_id,
46
+ eos_token_id=eos_token_id,
47
+ tie_word_embeddings=tie_word_embeddings,
48
+ **kwargs,
49
+ )
50
+ self.vocab_size = vocab_size
51
+ self.max_position_embeddings = max_position_embeddings
52
+ self.hidden_size = hidden_size
53
+ self.intermediate_size = intermediate_size
54
+ self.num_hidden_layers = num_hidden_layers
55
+ self.num_attention_heads = num_attention_heads
56
+ self.num_key_value_heads = num_key_value_heads
57
+ self.head_dim = head_dim
58
+ self.hidden_act = hidden_act
59
+ self.initializer_range = initializer_range
60
+ self.rms_norm_eps = rms_norm_eps
61
+ self.use_cache = use_cache
62
+ self.rope_theta = rope_theta
63
+ self.bits = bits
64
+ self.group_size = group_size
65
+ self.hadamard_block_size = hadamard_block_size
66
+ self.residual_rank = residual_rank
67
+ self.bifurcation_rank = bifurcation_rank
68
+ self.k_proj_rank = k_proj_rank
69
+ self.layer_rank_map = dict(layer_rank_map or {})
70
+ self.dense_tensor_names = list(dense_tensor_names or [])
71
+ self.int8_tensor_names = list(int8_tensor_names or [])
72
+ self.rotation_mode = rotation_mode
73
+ self.rotation_seed = int(rotation_seed)
74
+ self.tau_focus = tau_focus
75
+ self.haze_floor_margin = haze_floor_margin
kv_bss.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ KV-BSS: Key-Value Binding Softmax Sharpening.
3
+ """
4
+
5
+ import math
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+
10
+
11
+ class KVBSSAttentionHook(nn.Module):
12
+ def __init__(self, tau_focus: float = 1.10, haze_floor_margin: float = 12.0):
13
+ super().__init__()
14
+ if not math.isfinite(float(tau_focus)) or float(tau_focus) <= 0:
15
+ raise ValueError("tau_focus must be finite and greater than zero")
16
+ if not math.isfinite(float(haze_floor_margin)) or float(haze_floor_margin) < 0:
17
+ raise ValueError("haze_floor_margin must be finite and non-negative")
18
+ self.tau_focus = float(tau_focus)
19
+ self.haze_floor_margin = float(haze_floor_margin)
20
+
21
+ @staticmethod
22
+ def _prepare_mask(attention_mask, scores: torch.Tensor):
23
+ """Normalize 2D/4D masks and return additive mask plus hard-valid mask."""
24
+ b, h, q_len, kv_len = scores.shape
25
+ min_value = torch.finfo(scores.dtype).min
26
+
27
+ if attention_mask is None:
28
+ shape = (1, 1, 1, kv_len)
29
+ additive = torch.zeros(shape, dtype=scores.dtype, device=scores.device)
30
+ valid = torch.ones(shape, dtype=torch.bool, device=scores.device)
31
+ return additive, valid
32
+
33
+ mask = attention_mask.to(device=scores.device)
34
+ if mask.dim() == 2:
35
+ if tuple(mask.shape) != (b, kv_len):
36
+ raise ValueError(
37
+ "2D attention_mask must have shape "
38
+ f"({b}, {kv_len}), got {tuple(mask.shape)}"
39
+ )
40
+ valid = mask if mask.dtype == torch.bool else mask > 0
41
+ valid = valid[:, None, None, :]
42
+ additive = torch.where(
43
+ valid,
44
+ torch.zeros((), dtype=scores.dtype, device=scores.device),
45
+ torch.full((), min_value, dtype=scores.dtype, device=scores.device),
46
+ )
47
+ return additive, valid
48
+
49
+ if mask.dim() != 4:
50
+ raise ValueError("attention_mask must be None, 2D, or 4D")
51
+ try:
52
+ mask = torch.broadcast_to(mask, (b, h, q_len, kv_len))
53
+ except RuntimeError as exc:
54
+ raise ValueError(
55
+ "4D attention_mask is not broadcastable to "
56
+ f"{tuple(scores.shape)}, got {tuple(mask.shape)}"
57
+ ) from exc
58
+
59
+ if mask.dtype == torch.bool:
60
+ valid = mask
61
+ additive = torch.where(
62
+ valid,
63
+ torch.zeros((), dtype=scores.dtype, device=scores.device),
64
+ torch.full((), min_value, dtype=scores.dtype, device=scores.device),
65
+ )
66
+ else:
67
+ additive = mask.to(dtype=scores.dtype)
68
+ if torch.isnan(additive).any() or torch.isposinf(additive).any():
69
+ raise ValueError("attention_mask contains NaN or positive infinity")
70
+ # Hugging Face additive causal masks use finfo.min or -inf for
71
+ # blocked positions. Finite negative biases remain valid scores.
72
+ valid = torch.isfinite(additive) & (additive > min_value / 2)
73
+ return additive, valid
74
+
75
+ def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, attention_mask=None, scaling=None):
76
+ if query.dim() != 4 or key.dim() != 4 or value.dim() != 4:
77
+ raise ValueError("query, key, and value must all be 4D tensors")
78
+ b, h, q_len, d = query.shape
79
+ kb, kv_h, kv_len, kd = key.shape
80
+ vb, value_kv_h, value_kv_len, vd = value.shape
81
+ if (kb, value_kv_h, value_kv_len, vd) != (b, kv_h, kv_len, kd):
82
+ raise ValueError("key and value shapes must match in batch, heads, length, and depth")
83
+ if h == 0 or kv_h == 0 or h % kv_h != 0:
84
+ raise ValueError(f"query heads ({h}) must be a positive multiple of KV heads ({kv_h})")
85
+
86
+ if h != kv_h:
87
+ num_repeat = h // kv_h
88
+ key = key.repeat_interleave(num_repeat, dim=1)
89
+ value = value.repeat_interleave(num_repeat, dim=1)
90
+
91
+ if scaling is None:
92
+ scaling = 1.0 / math.sqrt(d)
93
+ if not math.isfinite(float(scaling)) or float(scaling) <= 0:
94
+ raise ValueError("scaling must be finite and greater than zero")
95
+
96
+ # Attention score accumulation in BF16 can overflow even when most
97
+ # individual activations are finite. Compute the score path in FP32 and
98
+ # isolate invalid query/key positions instead of taking down generation.
99
+ query_valid = torch.isfinite(query).all(dim=-1, keepdim=True)
100
+ key_valid = torch.isfinite(key).all(dim=-1).unsqueeze(-2)
101
+ query_fp32 = torch.nan_to_num(query.float(), nan=0.0, posinf=0.0, neginf=0.0)
102
+ key_fp32 = torch.nan_to_num(key.float(), nan=0.0, posinf=0.0, neginf=0.0)
103
+ scores = torch.matmul(query_fp32, key_fp32.transpose(-1, -2)) * float(scaling)
104
+ scores = scores * self.tau_focus
105
+ score_valid = torch.isfinite(scores)
106
+ scores = torch.nan_to_num(scores, nan=0.0, posinf=0.0, neginf=0.0)
107
+
108
+ additive_mask, hard_valid = self._prepare_mask(attention_mask, scores)
109
+ scores = scores + additive_mask
110
+ hard_valid = hard_valid.expand_as(scores)
111
+ hard_valid = hard_valid & score_valid & query_valid & key_valid
112
+
113
+ row_has_valid = hard_valid.any(dim=-1, keepdim=True)
114
+ neg_inf = torch.tensor(float("-inf"), dtype=scores.dtype, device=scores.device)
115
+ max_scores = scores.masked_fill(~hard_valid, neg_inf).amax(dim=-1, keepdim=True)
116
+ keep = hard_valid & (scores >= (max_scores - self.haze_floor_margin))
117
+ # Use the dtype minimum instead of a finite magic number so masked
118
+ # positions cannot receive probability mass at any supported dtype.
119
+ scores = scores.masked_fill(~keep, torch.finfo(scores.dtype).min)
120
+
121
+ probs = F.softmax(scores, dim=-1, dtype=torch.float32)
122
+ # A fully masked row is invalid input for ordinary softmax. Returning
123
+ # a finite zero vector keeps the failure contained and avoids NaN
124
+ # propagation through a whole decoder stack.
125
+ probs = probs * row_has_valid.to(dtype=probs.dtype)
126
+ value_fp32 = torch.nan_to_num(value.float(), nan=0.0, posinf=0.0, neginf=0.0)
127
+ out = torch.matmul(probs, value_fp32)
128
+ return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0).to(query.dtype)
modeling_minicpm_hadamard.py ADDED
@@ -0,0 +1,590 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PyTorch modeling implementation for MiniCPM5-2B-Hadamard-GSQ.
3
+ Engineered at F-Labs.
4
+
5
+ Features:
6
+ - HadamardLinear4bit: Group-wise INT4 with Walsh-Hadamard spin and Low-Rank SVD (SRC)
7
+ - Int8Linear / DenseBF16Linear: explicit mixed-precision paths for measured risk tiers
8
+ - ZeroCompressionShield: Pure BF16 RMSNorms, Biases, and Embeddings
9
+ - Adaptive L0-L3 rank allocation from calibration, spectral spikes, jumps, and curvature
10
+ - Key-Projection Sensitivity Defense (r=k_proj_rank on k_proj)
11
+ - KVBSSAttentionHook: Key-Value Binding Softmax Sharpening for 128K context
12
+ - RoPE (GPT-NeoX style, theta=rope_theta, head_dim=128) + causal mask + KV-cache
13
+ - Full compliance with Hugging Face PreTrainedModel standards.
14
+ """
15
+
16
+ import math
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+ from typing import Optional, Tuple
21
+ from transformers.modeling_utils import PreTrainedModel
22
+ from transformers.generation import GenerationMixin
23
+ from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
24
+
25
+ try:
26
+ from .configuration_minicpm_hadamard import MiniCPMHadamardConfig
27
+ from .kv_bss import KVBSSAttentionHook
28
+ except (ImportError, ValueError):
29
+ from configuration_minicpm_hadamard import MiniCPMHadamardConfig
30
+ from kv_bss import KVBSSAttentionHook
31
+
32
+ _H_CACHE = {}
33
+
34
+
35
+ def rademacher_signs(size: int, seed: int = 1729, dtype=torch.float32, device=None):
36
+ """Reproduce the calibration-time +/-1 input spin without storing a vector."""
37
+ generator = torch.Generator(device="cpu")
38
+ generator.manual_seed((int(seed) + 1009 * int(size)) % (2**63 - 1))
39
+ bits = torch.randint(0, 2, (size,), generator=generator, dtype=torch.int8)
40
+ return bits.to(torch.float32).mul_(2.0).sub_(1.0).to(device=device, dtype=dtype)
41
+
42
+
43
+ def layer_residual_rank(config, layer_idx: int, fallback: int = 16) -> int:
44
+ """Read a measured per-layer rank map, retaining the legacy fallback."""
45
+ rank_map = getattr(config, "layer_rank_map", None)
46
+ if isinstance(rank_map, dict):
47
+ value = rank_map.get(str(layer_idx), rank_map.get(layer_idx))
48
+ if value is not None:
49
+ return max(0, int(value))
50
+ if 14 <= layer_idx <= 27:
51
+ return int(getattr(config, "bifurcation_rank", 24))
52
+ return int(getattr(config, "residual_rank", fallback))
53
+
54
+
55
+ def get_hadamard_matrix(n: int, dtype=torch.float32, device=None):
56
+ key = (n, dtype, str(device))
57
+ if key in _H_CACHE:
58
+ return _H_CACHE[key]
59
+ if n == 1:
60
+ h = torch.tensor([[1.0]], dtype=dtype, device=device)
61
+ else:
62
+ h_half = get_hadamard_matrix(n // 2, dtype=dtype, device=device)
63
+ top = torch.cat([h_half, h_half], dim=1)
64
+ bottom = torch.cat([h_half, -h_half], dim=1)
65
+ h = torch.cat([top, bottom], dim=0) / math.sqrt(2.0)
66
+ _H_CACHE[key] = h
67
+ return h
68
+
69
+
70
+ def apply_hadamard_rot(x: torch.Tensor, block_size: int = 128) -> torch.Tensor:
71
+ orig_shape = x.shape
72
+ d = orig_shape[-1]
73
+ if d % block_size != 0:
74
+ return x
75
+ h = get_hadamard_matrix(block_size, dtype=x.dtype, device=x.device)
76
+ reshaped = x.view(-1, d // block_size, block_size)
77
+ rotated = torch.matmul(reshaped, h)
78
+ return rotated.view(orig_shape)
79
+
80
+
81
+ def apply_runtime_input_rotation(
82
+ x: torch.Tensor,
83
+ block_size: int,
84
+ rotation_mode: str,
85
+ rotation_signs: torch.Tensor,
86
+ ) -> torch.Tensor:
87
+ if x.shape[-1] % block_size != 0:
88
+ return x
89
+ if rotation_mode == "rademacher_hadamard":
90
+ x = x * rotation_signs.to(dtype=x.dtype, device=x.device)
91
+ return apply_hadamard_rot(x, block_size=block_size)
92
+
93
+
94
+ def resolve_rotation_signs(
95
+ signs: torch.Tensor,
96
+ size: int,
97
+ seed: int,
98
+ x: torch.Tensor,
99
+ ) -> torch.Tensor:
100
+ """Recover non-persistent signs after low_cpu_mem_usage meta init."""
101
+ if signs.device.type == "meta" or signs.numel() != size or not bool(signs.any()):
102
+ return rademacher_signs(size, seed, dtype=x.dtype, device=x.device)
103
+ return signs.to(dtype=x.dtype, device=x.device)
104
+
105
+
106
+ class DenseBF16Linear(nn.Module):
107
+ """Mixed-precision escape hatch for measured high-sensitivity projections."""
108
+
109
+ def __init__(self, in_features: int, out_features: int, block_size: int = 128,
110
+ rotation_mode: str = "fixed_hadamard", rotation_seed: int = 1729):
111
+ super().__init__()
112
+ self.in_features = in_features
113
+ self.out_features = out_features
114
+ self.block_size = block_size
115
+ self.rotation_mode = rotation_mode
116
+ self.rotation_seed = int(rotation_seed)
117
+ if rotation_mode == "rademacher_hadamard":
118
+ signs = rademacher_signs(in_features, self.rotation_seed, dtype=torch.float32)
119
+ else:
120
+ signs = torch.ones(in_features, dtype=torch.float32)
121
+ self.register_buffer("rotation_signs", signs, persistent=False)
122
+ self.register_buffer(
123
+ "weight_bf16",
124
+ torch.zeros((out_features, in_features), dtype=torch.bfloat16),
125
+ )
126
+
127
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
128
+ signs = resolve_rotation_signs(
129
+ self.rotation_signs, self.in_features, self.rotation_seed, x
130
+ )
131
+ x_rot = apply_runtime_input_rotation(
132
+ x, self.block_size, self.rotation_mode, signs
133
+ )
134
+ return F.linear(x_rot, self.weight_bf16.to(dtype=x.dtype, device=x.device))
135
+
136
+
137
+ class Int8Linear(nn.Module):
138
+ """Groupwise INT8 path for medium-risk projections."""
139
+
140
+ def __init__(self, in_features: int, out_features: int, group_size: int = 64,
141
+ rank: int = 16, block_size: int = 128,
142
+ rotation_mode: str = "fixed_hadamard", rotation_seed: int = 1729):
143
+ super().__init__()
144
+ self.in_features = in_features
145
+ self.out_features = out_features
146
+ self.group_size = group_size
147
+ self.rank = rank
148
+ self.block_size = block_size
149
+ self.rotation_mode = rotation_mode
150
+ self.rotation_seed = int(rotation_seed)
151
+ if rotation_mode == "rademacher_hadamard":
152
+ signs = rademacher_signs(in_features, self.rotation_seed, dtype=torch.float32)
153
+ else:
154
+ signs = torch.ones(in_features, dtype=torch.float32)
155
+ self.register_buffer("rotation_signs", signs, persistent=False)
156
+ self.register_buffer("qweight_int8", torch.zeros((out_features, in_features), dtype=torch.int8))
157
+ self.register_buffer("scales_int8", torch.zeros((out_features, in_features // group_size), dtype=torch.bfloat16))
158
+ self.register_buffer("svd_a", torch.zeros((out_features, rank), dtype=torch.bfloat16))
159
+ self.register_buffer("svd_b", torch.zeros((rank, in_features), dtype=torch.bfloat16))
160
+
161
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
162
+ signs = resolve_rotation_signs(
163
+ self.rotation_signs, self.in_features, self.rotation_seed, x
164
+ )
165
+ x_rot = apply_runtime_input_rotation(
166
+ x, self.block_size, self.rotation_mode, signs
167
+ )
168
+ m, n = self.qweight_int8.shape
169
+ w_float = (
170
+ self.qweight_int8.float().view(m, n // self.group_size, self.group_size)
171
+ * self.scales_int8.to(dtype=torch.float32).unsqueeze(-1)
172
+ ).view(m, n).to(dtype=x.dtype)
173
+ out_base = F.linear(x_rot, w_float)
174
+ x_svd = F.linear(x_rot, self.svd_b.to(dtype=x.dtype, device=x.device))
175
+ out_res = F.linear(x_svd, self.svd_a.to(dtype=x.dtype, device=x.device))
176
+ return out_base + out_res
177
+
178
+
179
+ class HadamardLinear4bit(nn.Module):
180
+ """
181
+ 4-bit Group-Scale Quantized Linear layer with:
182
+ 1. Walsh-Hadamard Input Coordinate Spin (Outlier suppression)
183
+ 2. Group-Scale INT4 quantization (group size 64)
184
+ 3. Low-Rank Residual SVD Compensation: Y = X' W_quant^T + (X' B^T) A^T
185
+ """
186
+
187
+ def __init__(self, in_features: int, out_features: int, group_size: int = 64,
188
+ rank: int = 16, block_size: int = 128, rotation_mode: str = "fixed_hadamard",
189
+ rotation_seed: int = 1729):
190
+ super().__init__()
191
+ self.in_features = in_features
192
+ self.out_features = out_features
193
+ self.group_size = group_size
194
+ self.rank = rank
195
+ self.block_size = block_size
196
+ self.rotation_mode = rotation_mode
197
+ self.rotation_seed = int(rotation_seed)
198
+
199
+ if self.rotation_mode == "rademacher_hadamard":
200
+ signs = rademacher_signs(
201
+ in_features, self.rotation_seed, dtype=torch.float32
202
+ )
203
+ else:
204
+ signs = torch.ones(in_features, dtype=torch.float32)
205
+ self.register_buffer("rotation_signs", signs, persistent=False)
206
+
207
+ self.register_buffer("qweight_packed", torch.zeros((out_features, in_features // 2), dtype=torch.uint8))
208
+ self.register_buffer("scales", torch.zeros((out_features, in_features // group_size), dtype=torch.bfloat16))
209
+ self.register_buffer("svd_a", torch.zeros((out_features, rank), dtype=torch.bfloat16))
210
+ self.register_buffer("svd_b", torch.zeros((rank, in_features), dtype=torch.bfloat16))
211
+
212
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
213
+ signs = resolve_rotation_signs(
214
+ self.rotation_signs, self.in_features, self.rotation_seed, x
215
+ )
216
+ x_rot = apply_runtime_input_rotation(
217
+ x, self.block_size, self.rotation_mode, signs
218
+ )
219
+
220
+ low = (self.qweight_packed & 0x0F).to(torch.int8)
221
+ low = torch.where(low >= 8, low - 16, low)
222
+ high = ((self.qweight_packed >> 4) & 0x0F).to(torch.int8)
223
+ high = torch.where(high >= 8, high - 16, high)
224
+
225
+ m, half_n = self.qweight_packed.shape
226
+ w_int8 = torch.empty((m, half_n * 2), dtype=torch.int8, device=x.device)
227
+ w_int8[:, 0::2] = low
228
+ w_int8[:, 1::2] = high
229
+
230
+ w_float = (w_int8.float().view(m, self.in_features // self.group_size, self.group_size) * self.scales.unsqueeze(-1)).view(m, self.in_features).to(x.dtype)
231
+ out_base = F.linear(x_rot, w_float)
232
+
233
+ x_svd = F.linear(x_rot, self.svd_b.to(x.dtype))
234
+ out_res = F.linear(x_svd, self.svd_a.to(x.dtype))
235
+
236
+ return out_base + out_res
237
+
238
+
239
+ def make_projection(config, full_name: str, in_features: int, out_features: int,
240
+ rank: int, linear_kwargs):
241
+ dense_names = getattr(config, "dense_tensor_names", ())
242
+ int8_names = getattr(config, "int8_tensor_names", ())
243
+ if full_name in dense_names:
244
+ return DenseBF16Linear(in_features, out_features, **{
245
+ key: linear_kwargs[key]
246
+ for key in ("block_size", "rotation_mode", "rotation_seed")
247
+ })
248
+ if full_name in int8_names:
249
+ return Int8Linear(in_features, out_features, rank=rank, **linear_kwargs)
250
+ return HadamardLinear4bit(
251
+ in_features, out_features, rank=rank, **linear_kwargs
252
+ )
253
+
254
+
255
+ class MiniCPMRMSNorm(nn.Module):
256
+ # ZeroCompressionShield: stays BF16, never quantized.
257
+ def __init__(self, hidden_size, eps=1e-6):
258
+ super().__init__()
259
+ self.weight = nn.Parameter(torch.ones(hidden_size, dtype=torch.bfloat16))
260
+ self.variance_epsilon = eps
261
+
262
+ def forward(self, hidden_states):
263
+ input_dtype = hidden_states.dtype
264
+ hidden_states = hidden_states.to(torch.float32)
265
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
266
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
267
+ return (self.weight.to(torch.float32) * hidden_states).to(input_dtype)
268
+
269
+
270
+ class MiniCPMHadamardRotaryEmbedding(nn.Module):
271
+ # GPT-NeoX style RoPE. inv_freq built from rope_theta; cached cos/sin up to max_pos.
272
+ def __init__(self, head_dim: int = 128, rope_theta: float = 5000000.0, max_position_embeddings: int = 131072):
273
+ super().__init__()
274
+ self.head_dim = head_dim
275
+ inv_freq = 1.0 / (rope_theta ** (torch.arange(0, head_dim, 2).float() / head_dim))
276
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
277
+ self.max_position_embeddings = max_position_embeddings
278
+ self._cached_cos = None
279
+ self._cached_sin = None
280
+ self._cached_len = 0
281
+
282
+ def _build_cache(self, seq_len: int, device, dtype):
283
+ t = torch.arange(seq_len, device=device, dtype=self.inv_freq.dtype)
284
+ freqs = torch.outer(t, self.inv_freq) # (seq_len, head_dim/2)
285
+ emb = torch.cat([freqs, freqs], dim=-1) # (seq_len, head_dim)
286
+ self._cached_cos = emb.cos().to(dtype)
287
+ self._cached_sin = emb.sin().to(dtype)
288
+ self._cached_len = seq_len
289
+
290
+ def forward(self, x: torch.Tensor, position_ids: torch.Tensor):
291
+ # x: (b, heads, seq, head_dim) — only used for device/dtype.
292
+ seq_len = int(position_ids.max().item()) + 1
293
+ if self._cached_cos is None or seq_len > self._cached_len or self._cached_cos.device != x.device:
294
+ self._build_cache(seq_len, x.device, torch.float32)
295
+ cos = self._cached_cos[position_ids].to(x.dtype) # (b, seq, head_dim)
296
+ sin = self._cached_sin[position_ids].to(x.dtype)
297
+ return cos.unsqueeze(1), sin.unsqueeze(1) # (b, 1, seq, head_dim) — broadcast over heads
298
+
299
+
300
+ def rotate_half(x: torch.Tensor) -> torch.Tensor:
301
+ d = x.shape[-1]
302
+ x1 = x[..., : d // 2]
303
+ x2 = x[..., d // 2 :]
304
+ return torch.cat([-x2, x1], dim=-1)
305
+
306
+
307
+ def apply_rotary_pos_emb(q, k, cos, sin):
308
+ # cos/sin: (b, 1, seq, head_dim), broadcast over heads.
309
+ q_embed = (q * cos) + (rotate_half(q) * sin)
310
+ k_embed = (k * cos) + (rotate_half(k) * sin)
311
+ return q_embed, k_embed
312
+
313
+
314
+ def _repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:
315
+ if n_rep == 1:
316
+ return x
317
+ return x.repeat_interleave(n_rep, dim=1)
318
+
319
+
320
+ def _make_causal_mask_4d(attention_mask_2d, q_len: int, kv_len: int, device, dtype) -> Optional[torch.Tensor]:
321
+ # Returns additive (b, 1, q_len, kv_len) mask: 0.0 keep / -inf block.
322
+ past_len = kv_len - q_len
323
+ causal = torch.full((q_len, kv_len), torch.finfo(dtype).min, device=device)
324
+ # allow j <= past_len + i
325
+ causal = torch.where(
326
+ torch.ones(q_len, kv_len, device=device, dtype=torch.bool).tril(diagonal=past_len),
327
+ torch.zeros((), device=device, dtype=dtype),
328
+ causal.to(dtype),
329
+ ) # (q, kv)
330
+ mask_4d = causal.unsqueeze(0).unsqueeze(0) # (1, 1, q, kv)
331
+ if attention_mask_2d is not None:
332
+ am = attention_mask_2d.to(dtype) # (b, kv_len)
333
+ additive = (1.0 - am) * torch.finfo(dtype).min # pad -> -inf
334
+ mask_4d = mask_4d + additive[:, None, None, :]
335
+ return mask_4d
336
+
337
+
338
+ class MiniCPMAttention(nn.Module):
339
+ def __init__(self, config: MiniCPMHadamardConfig, layer_idx: int = 0):
340
+ super().__init__()
341
+ self.config = config
342
+ self.layer_idx = layer_idx
343
+ self.hidden_size = config.hidden_size
344
+ self.num_heads = config.num_attention_heads
345
+ self.head_dim = config.head_dim
346
+ self.num_key_value_heads = config.num_key_value_heads
347
+
348
+ res_rank = layer_residual_rank(config, layer_idx)
349
+ k_rank = getattr(config, "k_proj_rank", 32)
350
+
351
+ linear_kwargs = dict(
352
+ group_size=config.group_size,
353
+ block_size=getattr(config, "hadamard_block_size", 128),
354
+ rotation_mode=getattr(config, "rotation_mode", "fixed_hadamard"),
355
+ rotation_seed=getattr(config, "rotation_seed", 1729),
356
+ )
357
+ prefix = f"model.layers.{layer_idx}.self_attn"
358
+ self.k_proj = make_projection(config, f"{prefix}.k_proj", self.hidden_size, self.num_key_value_heads * self.head_dim, k_rank, linear_kwargs)
359
+ self.q_proj = make_projection(config, f"{prefix}.q_proj", self.hidden_size, self.num_heads * self.head_dim, res_rank, linear_kwargs)
360
+ self.v_proj = make_projection(config, f"{prefix}.v_proj", self.hidden_size, self.num_key_value_heads * self.head_dim, res_rank, linear_kwargs)
361
+ self.o_proj = make_projection(config, f"{prefix}.o_proj", self.num_heads * self.head_dim, self.hidden_size, res_rank, linear_kwargs)
362
+
363
+ self.kv_bss = KVBSSAttentionHook(tau_focus=config.tau_focus, haze_floor_margin=config.haze_floor_margin)
364
+
365
+ def forward(
366
+ self,
367
+ hidden_states: torch.Tensor,
368
+ attention_mask: Optional[torch.Tensor] = None, # 4D additive, built by model
369
+ position_ids: Optional[torch.Tensor] = None,
370
+ past_key_values=None, # DynamicCache or tuple[(k,v)]
371
+ use_cache: bool = False,
372
+ cache_position: Optional[torch.Tensor] = None,
373
+ rotary_emb: Optional[MiniCPMHadamardRotaryEmbedding] = None,
374
+ ) -> Tuple[torch.Tensor, object]:
375
+ b, q_len, _ = hidden_states.shape
376
+ q = self.q_proj(hidden_states).view(b, q_len, self.num_heads, self.head_dim).transpose(1, 2)
377
+ k = self.k_proj(hidden_states).view(b, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
378
+ v = self.v_proj(hidden_states).view(b, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
379
+
380
+ # RoPE on fresh q/k only (past already rotated).
381
+ if rotary_emb is not None and position_ids is not None:
382
+ cos, sin = rotary_emb(q, position_ids)
383
+ q, k = apply_rotary_pos_emb(q, k, cos, sin)
384
+
385
+ # Append KV cache.
386
+ if past_key_values is not None:
387
+ if hasattr(past_key_values, "update"): # transformers DynamicCache / Cache
388
+ k, v = past_key_values.update(k, v, self.layer_idx)
389
+ else: # legacy tuple / list
390
+ pk, pv = past_key_values[self.layer_idx] if past_key_values[self.layer_idx] is not None else (None, None)
391
+ if pk is not None:
392
+ k = torch.cat([pk, k], dim=2)
393
+ v = torch.cat([pv, v], dim=2)
394
+ past_key_values[self.layer_idx] = (k, v)
395
+ elif use_cache:
396
+ past_key_values[self.layer_idx] = (k, v)
397
+ attn_out = self.kv_bss(q, k, v, attention_mask=attention_mask)
398
+ attn_out = attn_out.transpose(1, 2).contiguous().view(b, q_len, -1)
399
+ out = self.o_proj(attn_out)
400
+ return out, past_key_values
401
+
402
+
403
+ class MiniCPMMLP(nn.Module):
404
+ def __init__(self, config: MiniCPMHadamardConfig, layer_idx: int = 0):
405
+ super().__init__()
406
+ res_rank = layer_residual_rank(config, layer_idx)
407
+
408
+ linear_kwargs = dict(
409
+ group_size=config.group_size,
410
+ block_size=getattr(config, "hadamard_block_size", 128),
411
+ rotation_mode=getattr(config, "rotation_mode", "fixed_hadamard"),
412
+ rotation_seed=getattr(config, "rotation_seed", 1729),
413
+ )
414
+ prefix = f"model.layers.{layer_idx}.mlp"
415
+ self.gate_proj = make_projection(config, f"{prefix}.gate_proj", config.hidden_size, config.intermediate_size, res_rank, linear_kwargs)
416
+ self.up_proj = make_projection(config, f"{prefix}.up_proj", config.hidden_size, config.intermediate_size, res_rank, linear_kwargs)
417
+ self.down_proj = make_projection(config, f"{prefix}.down_proj", config.intermediate_size, config.hidden_size, res_rank, linear_kwargs)
418
+
419
+ def forward(self, x):
420
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
421
+
422
+
423
+ class MiniCPMDecoderLayer(nn.Module):
424
+ def __init__(self, config: MiniCPMHadamardConfig, layer_idx: int):
425
+ super().__init__()
426
+ self.input_layernorm = MiniCPMRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
427
+ self.self_attn = MiniCPMAttention(config, layer_idx=layer_idx)
428
+ self.post_attention_layernorm = MiniCPMRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
429
+ self.mlp = MiniCPMMLP(config, layer_idx=layer_idx)
430
+
431
+ def forward(self, hidden_states, attention_mask=None, position_ids=None,
432
+ past_key_values=None, use_cache=False, cache_position=None, rotary_emb=None):
433
+ residual = hidden_states
434
+ hidden_states = self.input_layernorm(hidden_states)
435
+ hidden_states, past_key_values = self.self_attn(
436
+ hidden_states, attention_mask=attention_mask, position_ids=position_ids,
437
+ past_key_values=past_key_values, use_cache=use_cache,
438
+ cache_position=cache_position, rotary_emb=rotary_emb,
439
+ )
440
+ hidden_states = residual + hidden_states
441
+
442
+ residual = hidden_states
443
+ hidden_states = self.post_attention_layernorm(hidden_states)
444
+ hidden_states = self.mlp(hidden_states)
445
+ hidden_states = residual + hidden_states
446
+ return hidden_states, past_key_values
447
+
448
+
449
+ class MiniCPMHadamardPreTrainedModel(PreTrainedModel):
450
+ config_class = MiniCPMHadamardConfig
451
+ base_model_prefix = "model"
452
+ supports_gradient_checkpointing = False
453
+ _no_split_modules = ["MiniCPMDecoderLayer"]
454
+
455
+ def _init_weights(self, module):
456
+ """Initialize scratch models while leaving quantized buffers untouched."""
457
+ if isinstance(module, (nn.Linear, nn.Embedding)):
458
+ std = float(getattr(self.config, "initializer_range", 0.02))
459
+ with torch.no_grad():
460
+ module.weight.normal_(mean=0.0, std=std)
461
+ if getattr(module, "bias", None) is not None:
462
+ module.bias.zero_()
463
+ elif isinstance(module, MiniCPMRMSNorm):
464
+ with torch.no_grad():
465
+ module.weight.fill_(1.0)
466
+
467
+
468
+ class MiniCPMHadamardModel(MiniCPMHadamardPreTrainedModel):
469
+ def __init__(self, config: MiniCPMHadamardConfig):
470
+ super().__init__(config)
471
+ self.padding_idx = getattr(config, "pad_token_id", 1)
472
+ self.vocab_size = config.vocab_size
473
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
474
+ self.layers = nn.ModuleList([
475
+ MiniCPMDecoderLayer(config, idx) for idx in range(config.num_hidden_layers)
476
+ ])
477
+ self.norm = MiniCPMRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
478
+ self.rotary_emb = MiniCPMHadamardRotaryEmbedding(
479
+ head_dim=config.head_dim, rope_theta=config.rope_theta,
480
+ max_position_embeddings=config.max_position_embeddings,
481
+ )
482
+ self.post_init()
483
+
484
+ def forward(self, input_ids=None, attention_mask=None, position_ids=None,
485
+ past_key_values=None, use_cache=None, cache_position=None,
486
+ inputs_embeds=None, **kwargs):
487
+ use_cache = self.config.use_cache if use_cache is None else use_cache
488
+ if inputs_embeds is not None:
489
+ x = inputs_embeds
490
+ b, q_len = x.shape[:2]
491
+ device = x.device
492
+ else:
493
+ b, q_len = input_ids.shape
494
+ device = input_ids.device
495
+ x = self.embed_tokens(input_ids)
496
+
497
+ # Past length for position ids / mask.
498
+ if past_key_values is not None and hasattr(past_key_values, "get_seq_length"):
499
+ past_len = past_key_values.get_seq_length()
500
+ elif past_key_values is not None and isinstance(past_key_values, (list, tuple)) and len(past_key_values) > 0 and past_key_values[0] is not None:
501
+ past_len = past_key_values[0][0].shape[2]
502
+ else:
503
+ past_len = 0
504
+
505
+ # NOTE (transformers>=5 compat): generate() passes trimmed input_ids with
506
+ # FULL-length position_ids (and sometimes a stale cache_position). Trusting
507
+ # them blindly lets RoPE broadcast-expand the query length, which breaks
508
+ # KV-BSS masking (scores kv != mask kv). Rebuild whenever shapes disagree.
509
+ if cache_position is None or cache_position.shape[0] != q_len:
510
+ cache_position = torch.arange(past_len, past_len + q_len, device=device)
511
+ if position_ids is None or position_ids.shape[-1] != q_len:
512
+ position_ids = cache_position.unsqueeze(0).expand(b, -1)
513
+
514
+ kv_len = past_len + q_len
515
+ mask_4d = None
516
+ if q_len > 1 or attention_mask is not None:
517
+ # Full 2D mask over kv window if user passed one, else causal only.
518
+ am_2d = None
519
+ if attention_mask is not None:
520
+ if attention_mask.dim() == 4:
521
+ mask_4d = attention_mask
522
+ elif attention_mask.dim() == 2:
523
+ am_2d = attention_mask
524
+ if mask_4d is None:
525
+ mask_4d = _make_causal_mask_4d(am_2d, q_len, kv_len, device, x.dtype)
526
+
527
+ # Legacy tuple cache init on first use_cache call.
528
+ if use_cache and past_key_values is None:
529
+ past_key_values = [None] * self.config.num_hidden_layers
530
+
531
+ for layer in self.layers:
532
+ x, past_key_values = layer(
533
+ x, attention_mask=mask_4d, position_ids=position_ids,
534
+ past_key_values=past_key_values, use_cache=use_cache,
535
+ cache_position=cache_position, rotary_emb=self.rotary_emb,
536
+ )
537
+ x = self.norm(x)
538
+ return BaseModelOutputWithPast(last_hidden_state=x, past_key_values=past_key_values)
539
+
540
+
541
+ class MiniCPMHadamardForCausalLM(MiniCPMHadamardPreTrainedModel, GenerationMixin):
542
+ def __init__(self, config: MiniCPMHadamardConfig):
543
+ super().__init__(config)
544
+ self.model = MiniCPMHadamardModel(config)
545
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
546
+ self.post_init()
547
+
548
+ def get_input_embeddings(self):
549
+ return self.model.embed_tokens
550
+
551
+ def set_input_embeddings(self, value):
552
+ self.model.embed_tokens = value
553
+
554
+ def get_output_embeddings(self):
555
+ return self.lm_head
556
+
557
+ def set_output_embeddings(self, new_embeddings):
558
+ self.lm_head = new_embeddings
559
+
560
+ def forward(self, input_ids=None, attention_mask=None, position_ids=None,
561
+ past_key_values=None, use_cache=None, cache_position=None,
562
+ inputs_embeds=None, labels=None, **kwargs):
563
+ hidden = self.model(
564
+ input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids,
565
+ past_key_values=past_key_values, use_cache=use_cache,
566
+ cache_position=cache_position, inputs_embeds=inputs_embeds, **kwargs,
567
+ )
568
+ logits = self.lm_head(hidden.last_hidden_state)
569
+ loss = None
570
+ if labels is not None:
571
+ loss = F.cross_entropy(logits.view(-1, logits.shape[-1]), labels.view(-1), ignore_index=-100)
572
+ return CausalLMOutputWithPast(
573
+ loss=loss, logits=logits, past_key_values=hidden.past_key_values,
574
+ )
575
+
576
+ def prepare_inputs_for_generation(self, input_ids, past_key_values=None, attention_mask=None, **kwargs):
577
+ # Trim to last token when cache is active.
578
+ if past_key_values is not None:
579
+ if isinstance(past_key_values, (list, tuple)):
580
+ past_len = past_key_values[0][0].shape[2] if past_key_values[0] is not None else 0
581
+ elif hasattr(past_key_values, "get_seq_length"):
582
+ past_len = past_key_values.get_seq_length()
583
+ else:
584
+ past_len = 0
585
+ if input_ids.shape[1] > past_len:
586
+ input_ids = input_ids[:, past_len:]
587
+ cache_position = kwargs.get("cache_position", None)
588
+ return {"input_ids": input_ids, "past_key_values": past_key_values,
589
+ "attention_mask": attention_mask, "cache_position": cache_position,
590
+ "position_ids": kwargs.get("position_ids", None), "use_cache": True}
test_inference.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Self-contained numerical checks for the MiniCPM5-2B-Hadamard-GSQ code."""
2
+
3
+ import unittest
4
+ import torch
5
+ from configuration_minicpm_hadamard import MiniCPMHadamardConfig
6
+ from modeling_minicpm_hadamard import MiniCPMHadamardForCausalLM
7
+ from kv_bss import KVBSSAttentionHook
8
+
9
+
10
+ def tiny_config(**overrides):
11
+ values = dict(
12
+ hidden_size=32,
13
+ intermediate_size=64,
14
+ num_hidden_layers=2,
15
+ num_attention_heads=4,
16
+ num_key_value_heads=2,
17
+ head_dim=8,
18
+ vocab_size=97,
19
+ group_size=8,
20
+ hadamard_block_size=8,
21
+ max_position_embeddings=64,
22
+ use_cache=True,
23
+ )
24
+ values.update(overrides)
25
+ return MiniCPMHadamardConfig(**values)
26
+
27
+
28
+ class MiniCPMNumericalTests(unittest.TestCase):
29
+ def test_architecture_forward_is_finite(self):
30
+ torch.manual_seed(0)
31
+ model = MiniCPMHadamardForCausalLM(tiny_config()).eval()
32
+ input_ids = torch.randint(0, 97, (1, 8))
33
+ with torch.no_grad():
34
+ out = model(input_ids, use_cache=False)
35
+ self.assertEqual(tuple(out.logits.shape), (1, 8, 97))
36
+ self.assertTrue(torch.isfinite(out.logits).all())
37
+
38
+ def test_kv_bss_mask_and_gqa_validation(self):
39
+ hook = KVBSSAttentionHook(tau_focus=1.0, haze_floor_margin=100.0)
40
+ query = torch.zeros(1, 2, 2, 4)
41
+ key = torch.zeros(1, 1, 3, 4)
42
+ value = torch.arange(12, dtype=torch.float32).view(1, 1, 3, 4)
43
+ # A 2D keep/pad mask must never route the padded value to the output.
44
+ out = hook(query, key, value, attention_mask=torch.tensor([[1, 1, 0]]))
45
+ expected = value[:, :, :2].mean(dim=2).unsqueeze(2).expand_as(out)
46
+ self.assertTrue(torch.allclose(out, expected))
47
+ # Fully masked rows remain finite and resolve to a zero vector.
48
+ blocked = torch.zeros(1, 3)
49
+ out_blocked = hook(query, key, value, attention_mask=blocked)
50
+ self.assertTrue(torch.isfinite(out_blocked).all())
51
+ self.assertEqual(float(out_blocked.abs().sum()), 0.0)
52
+ with self.assertRaises(ValueError):
53
+ bad_key = torch.zeros(1, 3, 3, 4)
54
+ bad_value = torch.zeros(1, 3, 3, 4)
55
+ hook(query, bad_key, bad_value)
56
+
57
+ def test_cached_and_uncached_logits_match(self):
58
+ torch.manual_seed(1)
59
+ model = MiniCPMHadamardForCausalLM(tiny_config()).eval()
60
+ input_ids = torch.tensor([[3, 7, 11, 19, 23]])
61
+ with torch.no_grad():
62
+ full = model(input_ids, use_cache=False).logits
63
+ prefix = model(input_ids[:, :3], use_cache=True)
64
+ suffix = model(
65
+ input_ids[:, 3:],
66
+ past_key_values=prefix.past_key_values,
67
+ use_cache=True,
68
+ )
69
+ self.assertTrue(torch.isfinite(suffix.logits).all())
70
+ torch.testing.assert_close(
71
+ suffix.logits, full[:, 3:], rtol=2e-4, atol=2e-5
72
+ )
73
+
74
+ def test_invalid_mask_is_rejected(self):
75
+ hook = KVBSSAttentionHook()
76
+ q = torch.zeros(1, 1, 1, 4)
77
+ k = torch.zeros(1, 1, 2, 4)
78
+ v = torch.zeros(1, 1, 2, 4)
79
+ with self.assertRaises(ValueError):
80
+ hook(q, k, v, attention_mask=torch.ones(1, 3))
81
+
82
+ def test_nonfinite_scores_are_contained(self):
83
+ hook = KVBSSAttentionHook(tau_focus=1.0, haze_floor_margin=12.0)
84
+ q = torch.zeros(1, 2, 2, 4, dtype=torch.bfloat16)
85
+ k = torch.zeros(1, 1, 2, 4, dtype=torch.bfloat16)
86
+ v = torch.ones(1, 1, 2, 4, dtype=torch.bfloat16)
87
+ q[0, 0, 0, 0] = float("nan")
88
+ k[0, 0, 1, 0] = float("inf")
89
+ out = hook(q, k, v)
90
+ self.assertTrue(torch.isfinite(out).all())
91
+
92
+ if __name__ == "__main__":
93
+ unittest.main(verbosity=2)