kgrabko commited on
Commit
1f76d06
·
verified ·
1 Parent(s): 0348311

Create JiRackTernaryPyTorch_1b_bitnet.py

Browse files
bitnet.cpp/JiRackTernaryPyTorch_1b_bitnet.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # COPYRIGHT (c) 2025-2026 Konstantin Vladimirovich Grabko. ALL RIGHTS RESERVED.
3
+ # CMS Manhattan JiRack Technology - PATENT PENDING
4
+ # Contact: grabko@cmsmanhattan.com
5
+ # =============================================================================
6
+ #
7
+ # JiRack Ternary Transformer 1B - "bitnet.cpp-compatible" revision
8
+ #
9
+ # Derived from JiRackTernaryPyTorch_1b.py (CMSManhattan/JiRackTernaryPro_1b).
10
+ # Three deliberate changes vs. that file, everything else preserved verbatim
11
+ # so the existing trained checkpoint loads 1:1 by name:
12
+ #
13
+ # [C-1] SubLN added. Two new learnable RMSNorm per block -
14
+ # attn_sub_norm (before out_proj) and ffn_sub_norm (before ffn_w2).
15
+ # bitnet.cpp's BITNET architecture hard-requires these tensors;
16
+ # without them the GGUF will not load. 16 layers x 2 = 32 new tensors.
17
+ #
18
+ # [C-2] lambda_ warmup knob added. The 1B original had NO lambda at all -
19
+ # BitLinear was always fully quantized. A lambda_ buffer per BitLinear
20
+ # (16 x 7 = 112) lets the network ease back into quantization while
21
+ # the brand-new SubLN weights learn their scales. Registered as a
22
+ # persistent buffer so it survives save/load.
23
+ #
24
+ # [C-3] Activation quantization no longer mean-centers.
25
+ # The original did `x_norm = x - x.mean(dim=-1)` before absmax.
26
+ # bitnet.cpp's I2_S/TL1/TL2 kernels implement plain absmax with NO
27
+ # mean subtraction, so keeping the centering would mean training
28
+ # against math the inference engine does not perform. Dropping it is
29
+ # a real behavioral change - this is part of why the fine-tune below
30
+ # is mandatory, not optional.
31
+ #
32
+ # RoPE IS UNCHANGED ON PURPOSE. The 1B original uses HF/Llama-style
33
+ # half-split rotation (rotate_half + freqs repeated as [f0..f31, f0..f31]).
34
+ # The 10B "_fixed" line used interleaved/NeoX instead. They are NOT
35
+ # interchangeable - the trained weights encode which one they expect.
36
+ # Consequence for export: the 1B converter MUST apply the standard
37
+ # convert_hf_to_gguf permute to q_proj/k_proj. The 10B converter must not.
38
+ # Do not copy one converter onto the other.
39
+ # =============================================================================
40
+
41
+ import math
42
+
43
+ import torch
44
+ import torch.nn as nn
45
+ import torch.nn.functional as F
46
+ from torch.utils.checkpoint import checkpoint
47
+
48
+ # ========================= JIRACK 1B CONFIG CONSTANTS =========================
49
+ VOCAB_SIZE = 128256
50
+ HIDDEN_SIZE = 2048
51
+ INTERMEDIATE_SIZE = 8192
52
+ NUM_LAYERS = 16
53
+ NUM_HEADS = 32
54
+ NUM_KV_HEADS = 8
55
+ HEAD_DIM = HIDDEN_SIZE // NUM_HEADS # = 64
56
+ MAX_SEQ_LEN = 4096
57
+ ROPE_THETA = 500000.0
58
+ RMS_EPS = 1e-6 # NOTE: 1e-6 here, the 10B line uses 1e-5
59
+ INIT_STD = 0.02
60
+
61
+ # --- quantization stability, from the original file ---
62
+ STABILITY_EPS = 1e-9
63
+ INT8_SCALE_TARGET = 127.0
64
+ QUANT_EPS = 1e-5 # clamp floor used inside BitLinear
65
+ # =============================================================================
66
+
67
+
68
+ def _detect_sdpa_gqa() -> bool:
69
+ try:
70
+ q = torch.zeros(1, 2, 1, 8)
71
+ kv = torch.zeros(1, 1, 1, 8)
72
+ F.scaled_dot_product_attention(q, kv, kv, enable_gqa=True)
73
+ return True
74
+ except TypeError:
75
+ return False
76
+ except Exception:
77
+ return False
78
+
79
+
80
+ _SDPA_HAS_GQA = _detect_sdpa_gqa()
81
+
82
+
83
+ class JiRackConfig1B:
84
+ def __init__(self):
85
+ self.vocab_size = VOCAB_SIZE
86
+ self.hidden_size = HIDDEN_SIZE
87
+ self.intermediate_size = INTERMEDIATE_SIZE
88
+ self.num_hidden_layers = NUM_LAYERS
89
+ self.num_attention_heads = NUM_HEADS
90
+ self.num_key_value_heads = NUM_KV_HEADS
91
+ self.head_dim = HEAD_DIM
92
+ self.max_seq_len = MAX_SEQ_LEN
93
+ self.max_position_embeddings = MAX_SEQ_LEN
94
+ self.rope_theta = ROPE_THETA
95
+ self.rms_norm_eps = RMS_EPS
96
+ self.init_std = INIT_STD
97
+
98
+
99
+ # ----------------------------------------------------------------------------
100
+ # RoPE - HF / Llama half-split style. IDENTICAL MATH to the 1B original.
101
+ # freqs has head_dim/2 = 32 entries; it is tiled to 64 as [f0..f31, f0..f31],
102
+ # and rotate_half splits the vector down the middle. This is what
103
+ # convert_hf_to_gguf.py's permute() is designed to undo, hence the converter
104
+ # for this model applies that permute.
105
+ # ----------------------------------------------------------------------------
106
+ def precompute_freqs_cis(dim: int, seq_len: int, theta: float = ROPE_THETA):
107
+ freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
108
+ t = torch.arange(seq_len).float()
109
+ freqs = torch.outer(t, freqs)
110
+ return torch.cos(freqs), torch.sin(freqs)
111
+
112
+
113
+ def apply_rotary_emb(xq, xk, freqs_cos, freqs_sin):
114
+ def rotate_half(x):
115
+ x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2:]
116
+ return torch.cat((-x2, x1), dim=-1)
117
+
118
+ T = xq.shape[2]
119
+ f_cos = freqs_cos[:T].to(device=xq.device, dtype=xq.dtype).view(1, 1, T, -1).repeat(1, 1, 1, 2)
120
+ f_sin = freqs_sin[:T].to(device=xq.device, dtype=xq.dtype).view(1, 1, T, -1).repeat(1, 1, 1, 2)
121
+
122
+ xq_out = (xq * f_cos) + (rotate_half(xq) * f_sin)
123
+ xk_out = (xk * f_cos) + (rotate_half(xk) * f_sin)
124
+ return xq_out, xk_out
125
+
126
+
127
+ class BitLinear(nn.Linear):
128
+ """Canonical BitNet b1.58 fake-quant linear.
129
+
130
+ No internal normalization - that is the caller's job (norm1/norm2 for the
131
+ pre-norms, attn_sub_norm/ffn_sub_norm for SubLN). Weight quant is absmean
132
+ ternary; activation quant is plain absmax int8, NO mean centering, see
133
+ [C-3] in the header.
134
+ """
135
+
136
+ def __init__(self, in_features, out_features, bias=False):
137
+ super().__init__(in_features, out_features, bias=bias)
138
+ self.eps = QUANT_EPS
139
+ # [C-2] persistent so it round-trips through save/load
140
+ self.register_buffer("lambda_", torch.zeros(()), persistent=True)
141
+
142
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
143
+ if not self.training and float(self.lambda_) < 1e-6:
144
+ return F.linear(x, self.weight, self.bias)
145
+
146
+ lam = self.lambda_.to(x.dtype)
147
+
148
+ w = self.weight
149
+ gamma = w.float().abs().mean().clamp(min=self.eps).to(w.dtype)
150
+ w_quant = torch.clamp(torch.round(w / gamma), -1.0, 1.0) * gamma
151
+ w_effective = w + lam * (w_quant - w).detach()
152
+
153
+ x_scale = INT8_SCALE_TARGET / x.abs().max(dim=-1, keepdim=True).values.clamp(min=self.eps)
154
+ x_quant = torch.clamp(torch.round(x * x_scale), -128.0, 127.0) / x_scale
155
+ x_effective = x + lam * (x_quant - x).detach()
156
+
157
+ return F.linear(x_effective, w_effective, self.bias)
158
+
159
+
160
+ class RMSNorm(nn.Module):
161
+ """Learnable RMSNorm. Used for the block pre-norms AND for SubLN -
162
+ same math, different position in the graph."""
163
+
164
+ def __init__(self, dim, eps=RMS_EPS):
165
+ super().__init__()
166
+ self.eps = eps
167
+ self.weight = nn.Parameter(torch.ones(dim))
168
+
169
+ def forward(self, x):
170
+ dtype = x.dtype
171
+ x = x.float()
172
+ x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
173
+ return (x * self.weight.float()).to(dtype)
174
+
175
+
176
+ def repeat_kv(x, n_rep):
177
+ if n_rep == 1:
178
+ return x
179
+ return x.repeat_interleave(n_rep, dim=1)
180
+
181
+
182
+ class TransformerBlock(nn.Module):
183
+ def __init__(self, config, use_checkpoint=False, bias=False):
184
+ super().__init__()
185
+ self.use_checkpoint = use_checkpoint
186
+ self.n_heads = config.num_attention_heads
187
+ self.n_kv_heads = config.num_key_value_heads
188
+ self.head_dim = config.head_dim
189
+ self.n_rep = self.n_heads // self.n_kv_heads
190
+
191
+ self.q_proj = BitLinear(config.hidden_size, config.hidden_size, bias=bias)
192
+ self.k_proj = BitLinear(config.hidden_size, self.n_kv_heads * self.head_dim, bias=bias)
193
+ self.v_proj = BitLinear(config.hidden_size, self.n_kv_heads * self.head_dim, bias=bias)
194
+ self.out_proj = BitLinear(config.hidden_size, config.hidden_size, bias=bias)
195
+
196
+ self.ffn_w1 = BitLinear(config.hidden_size, config.intermediate_size, bias=bias)
197
+ self.ffn_w3 = BitLinear(config.hidden_size, config.intermediate_size, bias=bias)
198
+ self.ffn_w2 = BitLinear(config.intermediate_size, config.hidden_size, bias=bias)
199
+
200
+ self.norm1 = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
201
+ self.norm2 = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
202
+
203
+ # [C-1] SubLN, exactly where bitnet.cpp expects it
204
+ self.attn_sub_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
205
+ self.ffn_sub_norm = RMSNorm(config.intermediate_size, eps=config.rms_norm_eps)
206
+
207
+ def forward(self, x, freqs_cos, freqs_sin):
208
+ if self.use_checkpoint and self.training:
209
+ return checkpoint(self._forward_impl, x, freqs_cos, freqs_sin, use_reentrant=False)
210
+ return self._forward_impl(x, freqs_cos, freqs_sin)
211
+
212
+ def _forward_impl(self, x, freqs_cos, freqs_sin):
213
+ h = self.norm1(x)
214
+ B, T, D = x.shape
215
+
216
+ q = self.q_proj(h).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
217
+ k = self.k_proj(h).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
218
+ v = self.v_proj(h).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
219
+
220
+ q, k = apply_rotary_emb(q, k, freqs_cos, freqs_sin)
221
+
222
+ if self.n_rep > 1 and _SDPA_HAS_GQA:
223
+ attn_out = F.scaled_dot_product_attention(q, k, v, is_causal=True, enable_gqa=True)
224
+ else:
225
+ k, v = repeat_kv(k, self.n_rep), repeat_kv(v, self.n_rep)
226
+ attn_out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
227
+
228
+ attn_out = attn_out.transpose(1, 2).contiguous().view(B, T, D)
229
+
230
+ # [C-1] SubLN before the ternary output projection
231
+ attn_out = self.attn_sub_norm(attn_out)
232
+ x = x + self.out_proj(attn_out)
233
+
234
+ m = self.norm2(x)
235
+ gate = F.silu(self.ffn_w1(m))
236
+ up = self.ffn_w3(m)
237
+ ffn_hidden = gate * up
238
+
239
+ # [C-1] SubLN before the ternary down projection
240
+ ffn_hidden = self.ffn_sub_norm(ffn_hidden)
241
+ x = x + self.ffn_w2(ffn_hidden)
242
+
243
+ return x
244
+
245
+
246
+ class JiRackTransformer1B(nn.Module):
247
+ def __init__(self, config: JiRackConfig1B = None, use_checkpoint=False, bias=False):
248
+ super().__init__()
249
+ self.config = config if config is not None else JiRackConfig1B()
250
+ self.use_checkpoint = use_checkpoint
251
+
252
+ self.token_emb = nn.Embedding(self.config.vocab_size, self.config.hidden_size)
253
+ self.blocks = nn.ModuleList([
254
+ TransformerBlock(self.config, self.use_checkpoint, bias=bias)
255
+ for _ in range(self.config.num_hidden_layers)
256
+ ])
257
+ self.ln_f = RMSNorm(self.config.hidden_size, eps=self.config.rms_norm_eps)
258
+ self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False)
259
+
260
+ cos, sin = precompute_freqs_cis(
261
+ dim=self.config.head_dim,
262
+ seq_len=self.config.max_seq_len,
263
+ theta=self.config.rope_theta,
264
+ )
265
+ # NOTE: the 1B original registered these as PERSISTENT buffers, so an
266
+ # old checkpoint may or may not carry freqs_cos/freqs_sin depending on
267
+ # how it was saved. Non-persistent here (they are recomputed at init);
268
+ # the migration script tolerates them being present in the old file.
269
+ self.register_buffer("freqs_cos", cos, persistent=False)
270
+ self.register_buffer("freqs_sin", sin, persistent=False)
271
+
272
+ self._init_weights()
273
+
274
+ def _init_weights(self):
275
+ std = self.config.init_std
276
+ resid_std = std / math.sqrt(2 * self.config.num_hidden_layers)
277
+
278
+ nn.init.normal_(self.token_emb.weight, mean=0.0, std=std)
279
+ nn.init.normal_(self.lm_head.weight, mean=0.0, std=std)
280
+
281
+ for block in self.blocks:
282
+ for lin in (block.q_proj, block.k_proj, block.v_proj, block.ffn_w1, block.ffn_w3):
283
+ nn.init.normal_(lin.weight, mean=0.0, std=std)
284
+ if lin.bias is not None:
285
+ nn.init.zeros_(lin.bias)
286
+ for lin in (block.out_proj, block.ffn_w2):
287
+ nn.init.normal_(lin.weight, mean=0.0, std=resid_std)
288
+ if lin.bias is not None:
289
+ nn.init.zeros_(lin.bias)
290
+ # SubLN weights stay at their RMSNorm init of 1.0 -> near-identity,
291
+ # which minimizes the shock of the architecture change.
292
+
293
+ def load_from_old_checkpoint(self, old_state_dict: dict):
294
+ """Load weights from the ORIGINAL 1B architecture (no SubLN, no lambda_).
295
+
296
+ Tensor names are identical between the two files, so this is a plain
297
+ strict=False load. Exactly three categories of difference are tolerated:
298
+
299
+ 1. missing *.sub_norm.weight - the 32 new SubLN tensors [C-1].
300
+ Left at init 1.0, must be learned during the fine-tune.
301
+ 2. missing *.lambda_ - the 112 new warmup buffers [C-2].
302
+ Left at 0.0, the correct start for a fresh warmup.
303
+ 3. unexpected freqs_cos/freqs_sin - the original registered its RoPE
304
+ tables as persistent buffers, so they may be baked into the old
305
+ file. They are recomputed at init here, so they are ignored.
306
+
307
+ Anything else is a real mismatch and raises.
308
+ """
309
+ missing, unexpected = self.load_state_dict(old_state_dict, strict=False)
310
+
311
+ new_subln = [m for m in missing if "sub_norm" in m]
312
+ new_lambda = [m for m in missing if m.endswith("lambda_")]
313
+ other_missing = [m for m in missing
314
+ if "sub_norm" not in m and not m.endswith("lambda_")]
315
+
316
+ rope_extra = [u for u in unexpected if u in ("freqs_cos", "freqs_sin")]
317
+ other_unexpected = [u for u in unexpected if u not in ("freqs_cos", "freqs_sin")]
318
+
319
+ if other_missing:
320
+ raise RuntimeError(
321
+ f"{len(other_missing)} unexpected missing keys beyond SubLN/lambda_: "
322
+ f"{other_missing[:10]}"
323
+ )
324
+ if other_unexpected:
325
+ raise RuntimeError(
326
+ f"{len(other_unexpected)} unexpected keys in old checkpoint: "
327
+ f"{other_unexpected[:10]}"
328
+ )
329
+
330
+ n_layers = self.config.num_hidden_layers
331
+ print("Loaded old 1B checkpoint.")
332
+ print(f" {len(new_subln)} SubLN tensors initialized at 1.0 "
333
+ f"(expected {2 * n_layers}), e.g. {new_subln[:2]}")
334
+ print(f" {len(new_lambda)} lambda_ buffers initialized at 0.0 "
335
+ f"(expected {7 * n_layers}) - warmup starts fresh")
336
+ if rope_extra:
337
+ print(f" ignored baked-in RoPE tables: {rope_extra}")
338
+
339
+ def set_lambda(self, lambda_value: float):
340
+ for module in self.modules():
341
+ if isinstance(module, BitLinear):
342
+ module.lambda_.fill_(lambda_value)
343
+
344
+ def get_lambda(self) -> float:
345
+ for module in self.modules():
346
+ if isinstance(module, BitLinear):
347
+ return float(module.lambda_)
348
+ return 0.0
349
+
350
+ def forward(self, input_ids):
351
+ x = self.token_emb(input_ids)
352
+ for block in self.blocks:
353
+ x = block(x, self.freqs_cos, self.freqs_sin)
354
+ return self.lm_head(self.ln_f(x))
355
+
356
+ @torch.no_grad()
357
+ def export_ternary_state_dict(self):
358
+ """Bake fake-quant weights into real ternary codes + per-tensor scale.
359
+ SubLN weights are full precision and pass through untouched, same as
360
+ embeddings / lm_head / norms."""
361
+ out = {}
362
+ for name, module in self.named_modules():
363
+ if isinstance(module, BitLinear):
364
+ w = module.weight.float()
365
+ gamma = w.abs().mean().clamp(min=module.eps)
366
+ codes = torch.clamp(torch.round(w / gamma), -1.0, 1.0).to(torch.int8)
367
+ out[f"{name}.codes"] = codes
368
+ out[f"{name}.gamma"] = gamma
369
+ if module.bias is not None:
370
+ out[f"{name}.bias"] = module.bias.detach().clone()
371
+ out["token_emb.weight"] = self.token_emb.weight.detach().clone()
372
+ out["lm_head.weight"] = self.lm_head.weight.detach().clone()
373
+ out["ln_f.weight"] = self.ln_f.weight.detach().clone()
374
+ for name, module in self.named_modules():
375
+ if isinstance(module, RMSNorm) and name != "ln_f":
376
+ out[f"{name}.weight"] = module.weight.detach().clone()
377
+ return out
378
+
379
+
380
+ if __name__ == "__main__":
381
+ class TinyConfig(JiRackConfig1B):
382
+ def __init__(self):
383
+ super().__init__()
384
+ self.vocab_size = 256
385
+ self.hidden_size = 64
386
+ self.intermediate_size = 128
387
+ self.num_hidden_layers = 2
388
+ self.num_attention_heads = 4
389
+ self.num_key_value_heads = 2
390
+ self.head_dim = 16
391
+ self.max_seq_len = 64
392
+
393
+ torch.manual_seed(0)
394
+ model = JiRackTransformer1B(TinyConfig()).eval()
395
+ ids = torch.randint(0, 256, (2, 32))
396
+
397
+ with torch.no_grad():
398
+ model.set_lambda(0.0)
399
+ y0 = model(ids)
400
+ model.set_lambda(1.0)
401
+ y1 = model(ids)
402
+
403
+ assert torch.isfinite(y0).all() and torch.isfinite(y1).all()
404
+ ratio = y1.std() / y0.std()
405
+ print(f"std ratio lambda=1 vs lambda=0: {ratio.item():.3f} (should be O(1))")
406
+
407
+ sd = model.state_dict()
408
+ subln = [n for n in sd if "sub_norm" in n]
409
+ lam = [n for n in sd if n.endswith("lambda_")]
410
+ print(f"SubLN tensors: {len(subln)} (expect 2 x n_layers)")
411
+ print(f"lambda_ buffers: {len(lam)} (expect 7 x n_layers)")
412
+ print("Smoke test OK.")