JiRackTernaryUltra_1b.py
19.8 KB · 470 lines · python Raw
1 #%%writefile JiRackTernaryUltra_1p5b.py
2 # =============================================================================
3 # COPYRIGHT © 2026 Konstantin Vladimirovich Grabko. ALL RIGHTS RESERVED.
4 # JiRack Ultra Ternary Transformer
5 #
6 # CMS Manhattan JiRack Technology — PATENT PENDING
7 #
8 # This code is proprietary.
9 # Personal and non-commercial research use is allowed.
10 # Any commercial use, derivative works for profit, or distribution
11 # requires a paid license and 5% royalty.
12 #
13 # Unauthorized commercial use is strictly prohibited.
14 # Contact: grabko@cmsmanhattan.com
15 # =============================================================================
16 import math
17
18 import torch
19 import torch.nn as nn
20 import torch.nn.functional as F
21 from torch.utils.checkpoint import checkpoint
22
23 # ==================== CONFIG CONSTANTS [DS1.5-1] ====================
24 VOCAB_SIZE = 151936
25 HIDDEN_SIZE = 1536
26 INTERMEDIATE_SIZE = 8960
27 NUM_LAYERS = 28
28 NUM_HEADS = 12
29 NUM_KV_HEADS = 2
30 HEAD_DIM = 128 # 12 * 128 = 1536 = hidden (q); kv dim = 2*128 = 256
31 MAX_SEQ_LEN = 4096 # [DS-6] raise for long-context (ckpt supports 131072)
32 ROPE_THETA = 10000.0 # Qwen2.5-Math value (NOT Llama-3's 500000)
33 RMS_EPS = 1e-6 # Qwen2 value (NOT Llama-3's 1e-5)
34 ROPE_SCALE_FACTOR = 1.0
35 INIT_STD = 0.02
36 ATTN_QKV_BIAS = True # [DS-2] Qwen2: bias on q/k/v only
37 # =================================================================
38
39
40 # [FIX-6] Feature-detect native GQA support in SDPA (PyTorch >= 2.5).
41 def _detect_sdpa_gqa() -> bool:
42 try:
43 q = torch.zeros(1, 2, 1, 8)
44 kv = torch.zeros(1, 1, 1, 8)
45 F.scaled_dot_product_attention(q, kv, kv, enable_gqa=True)
46 return True
47 except TypeError:
48 return False
49 except Exception:
50 return False
51
52 _SDPA_HAS_GQA = _detect_sdpa_gqa()
53
54
55 class JiRackConfig:
56 def __init__(self):
57 self.vocab_size = VOCAB_SIZE
58 self.hidden_size = HIDDEN_SIZE
59 self.intermediate_size = INTERMEDIATE_SIZE
60 self.num_hidden_layers = NUM_LAYERS
61 self.num_attention_heads = NUM_HEADS
62 self.num_key_value_heads = NUM_KV_HEADS
63 self.head_dim = HEAD_DIM
64 self.max_seq_len = MAX_SEQ_LEN
65 self.rope_theta = ROPE_THETA
66 self.rms_norm_eps = RMS_EPS
67 self.rope_scale_factor = ROPE_SCALE_FACTOR
68 self.init_std = INIT_STD
69 self.attn_qkv_bias = ATTN_QKV_BIAS
70
71
72 # ==================== RoPE — HALF-SPLIT (HF convention) [DS-3] ====================
73 def precompute_freqs_cis(
74 dim: int,
75 end: int,
76 theta: float = ROPE_THETA,
77 scale_factor: float = ROPE_SCALE_FACTOR,
78 ):
79 """cos/sin of shape (end, dim), HF half-split layout: the (dim/2)
80 frequency vector is CONCATENATED with itself (torch.cat), not
81 interleaved. Matches transformers' LlamaRotaryEmbedding/Qwen2."""
82 freqs = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
83 if scale_factor > 1.0:
84 freqs = freqs / scale_factor
85 t = torch.arange(end, dtype=torch.float32)
86 freqs = torch.outer(t, freqs) # (end, dim/2)
87 emb = torch.cat((freqs, freqs), dim=-1) # (end, dim) — half-split
88 return torch.cos(emb), torch.sin(emb)
89
90
91 def rotate_half(x):
92 """HF convention: (-x2, x1) where x1/x2 are the two HALVES of head_dim."""
93 x1 = x[..., : x.shape[-1] // 2]
94 x2 = x[..., x.shape[-1] // 2:]
95 return torch.cat((-x2, x1), dim=-1)
96
97
98 def apply_rotary_emb(xq, xk, cos, sin):
99 """Half-split RoPE, identical math to transformers.apply_rotary_pos_emb.
100 cos/sin: (T, head_dim); q/k: (B, H, T, head_dim)."""
101 cos = cos[None, None, :, :]
102 sin = sin[None, None, :, :]
103 xq_out = (xq * cos) + (rotate_half(xq) * sin)
104 xk_out = (xk * cos) + (rotate_half(xk) * sin)
105 return xq_out, xk_out
106
107
108 class BitLinear(nn.Linear):
109 """BitNet b1.58-style fake-quant linear with lambda warmup.
110 Identical to the 10B version ([FIX-1..5] preserved); bias — when
111 present ([DS-2]) — stays full precision ([DS-5])."""
112
113 def __init__(self, in_features, out_features, bias=False):
114 super().__init__(in_features, out_features, bias=bias)
115 self.eps = 1e-5
116 # [FIX-4] Buffer -> saved in state_dict, survives checkpoint resume.
117 self.register_buffer("lambda_", torch.zeros(()), persistent=True)
118
119 def forward(self, x: torch.Tensor) -> torch.Tensor:
120 # Fast path (exact at lambda=0 by continuity).
121 if not self.training and float(self.lambda_) < 1e-6:
122 return F.linear(x, self.weight, self.bias)
123
124 lam = self.lambda_.to(x.dtype)
125
126 # === Weights: per-tensor absmean ternary (b1.58) ===
127 w = self.weight
128 gamma = w.float().abs().mean().clamp(min=self.eps).to(w.dtype) # [FIX-5]
129 w_quant = torch.clamp(torch.round(w / gamma), -1.0, 1.0) * gamma
130 w_effective = w + lam * (w_quant - w).detach()
131
132 # === Activations: per-token absmax int8 ([FIX-2],[FIX-3]) ===
133 x_scale = 127.0 / x.abs().max(dim=-1, keepdim=True).values.clamp(min=self.eps)
134 x_quant = torch.clamp(torch.round(x * x_scale), -128.0, 127.0) / x_scale
135 x_effective = x + lam * (x_quant - x).detach()
136
137 # [FIX-1] Dequantized operands -> no post-matmul rescale.
138 # [DS-5] bias added in full precision by F.linear.
139 return F.linear(x_effective, w_effective, self.bias)
140
141
142 class RMSNorm(nn.Module):
143 def __init__(self, dim, eps=RMS_EPS):
144 super().__init__()
145 self.eps = eps
146 self.weight = nn.Parameter(torch.ones(dim))
147
148 def forward(self, x):
149 # [FIX-5] Compute statistics in fp32, cast back to input dtype.
150 dtype = x.dtype
151 x = x.float()
152 x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
153 return (x * self.weight.float()).to(dtype)
154
155
156 class TransformerBlock(nn.Module):
157 def __init__(self, config, use_checkpoint=False):
158 super().__init__()
159 self.use_checkpoint = use_checkpoint
160 self.n_heads = config.num_attention_heads
161 self.n_kv_heads = config.num_key_value_heads
162 self.head_dim = config.head_dim
163 self.n_rep = self.n_heads // self.n_kv_heads
164
165 self.norm1 = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
166 self.norm2 = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
167
168 qkv_bias = config.attn_qkv_bias # [DS-2]
169 self.q_proj = BitLinear(config.hidden_size,
170 self.n_heads * self.head_dim, bias=qkv_bias)
171 self.k_proj = BitLinear(config.hidden_size,
172 self.n_kv_heads * self.head_dim, bias=qkv_bias)
173 self.v_proj = BitLinear(config.hidden_size,
174 self.n_kv_heads * self.head_dim, bias=qkv_bias)
175 self.out_proj = BitLinear(self.n_heads * self.head_dim,
176 config.hidden_size, bias=False)
177
178 self.ffn_w1 = BitLinear(config.hidden_size, config.intermediate_size, bias=False) # gate
179 self.ffn_w3 = BitLinear(config.hidden_size, config.intermediate_size, bias=False) # up
180 self.ffn_w2 = BitLinear(config.intermediate_size, config.hidden_size, bias=False) # down
181
182 def forward(self, x, freqs_cos, freqs_sin):
183 if self.use_checkpoint and self.training:
184 return checkpoint(
185 self._forward_impl, x, freqs_cos, freqs_sin, use_reentrant=False
186 )
187 return self._forward_impl(x, freqs_cos, freqs_sin)
188
189 def _forward_impl(self, x, freqs_cos, freqs_sin):
190 h = self.norm1(x)
191 B, T, _ = h.shape
192
193 q = self.q_proj(h).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
194 k = self.k_proj(h).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
195 v = self.v_proj(h).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
196
197 q, k = apply_rotary_emb(q, k, freqs_cos, freqs_sin) # [DS-3] half-split
198
199 if self.n_rep > 1 and _SDPA_HAS_GQA: # [FIX-6]
200 attn_out = F.scaled_dot_product_attention(
201 q, k, v, is_causal=True, enable_gqa=True
202 )
203 else:
204 if self.n_rep > 1:
205 k = k.repeat_interleave(self.n_rep, dim=1)
206 v = v.repeat_interleave(self.n_rep, dim=1)
207 attn_out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
208
209 attn_out = attn_out.transpose(1, 2).contiguous().view(B, T, -1)
210
211 x = x + self.out_proj(attn_out)
212
213 m = self.norm2(x)
214 gate = F.silu(self.ffn_w1(m))
215 up = self.ffn_w3(m)
216 x = x + self.ffn_w2(gate * up)
217
218 return x
219
220
221 class JiRackTransformer(nn.Module):
222 def __init__(self, config: JiRackConfig = None, use_checkpoint=False):
223 super().__init__()
224 self.config = config if config is not None else JiRackConfig()
225 self.use_checkpoint = use_checkpoint
226
227 self.token_emb = nn.Embedding(self.config.vocab_size, self.config.hidden_size)
228 self.blocks = nn.ModuleList([
229 TransformerBlock(self.config, self.use_checkpoint)
230 for _ in range(self.config.num_hidden_layers)
231 ])
232 self.ln_f = RMSNorm(self.config.hidden_size, eps=self.config.rms_norm_eps)
233 # [DS1.5-2] tie_word_embeddings = False (verified) -> separate lm_head.
234 self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False)
235
236 cos, sin = precompute_freqs_cis(
237 dim=self.config.head_dim,
238 end=self.config.max_seq_len,
239 theta=self.config.rope_theta,
240 scale_factor=self.config.rope_scale_factor,
241 )
242 self.register_buffer("freqs_cos", cos, persistent=False)
243 self.register_buffer("freqs_sin", sin, persistent=False)
244
245 # [FIX-8] Only relevant when training from scratch; harmless before
246 # load_hf_state_dict() overwrites everything.
247 self._init_weights()
248
249 def _init_weights(self):
250 std = self.config.init_std
251 resid_std = std / math.sqrt(2 * self.config.num_hidden_layers)
252
253 nn.init.normal_(self.token_emb.weight, mean=0.0, std=std)
254 nn.init.normal_(self.lm_head.weight, mean=0.0, std=std)
255
256 for block in self.blocks:
257 for lin in (block.q_proj, block.k_proj, block.v_proj,
258 block.ffn_w1, block.ffn_w3):
259 nn.init.normal_(lin.weight, mean=0.0, std=std)
260 if lin.bias is not None:
261 nn.init.zeros_(lin.bias)
262 for lin in (block.out_proj, block.ffn_w2):
263 nn.init.normal_(lin.weight, mean=0.0, std=resid_std)
264 if lin.bias is not None:
265 nn.init.zeros_(lin.bias)
266
267 # ---------------- lambda warmup hooks (unchanged) ----------------
268 def set_lambda(self, lambda_value: float):
269 for module in self.modules():
270 if isinstance(module, BitLinear):
271 module.lambda_.fill_(lambda_value)
272
273 def get_lambda(self) -> float:
274 for module in self.modules():
275 if isinstance(module, BitLinear):
276 return float(module.lambda_)
277 return 0.0
278
279 def forward(self, input_ids):
280 seq_len = input_ids.shape[1]
281 x = self.token_emb(input_ids)
282
283 cos = self.freqs_cos[:seq_len].to(device=x.device, dtype=x.dtype)
284 sin = self.freqs_sin[:seq_len].to(device=x.device, dtype=x.dtype)
285
286 for block in self.blocks:
287 x = block(x, cos, sin)
288
289 return self.lm_head(self.ln_f(x))
290
291 # ------------------------------------------------------------------
292 # [DS-4] HF Qwen2 -> JiRack weight mapping.
293 # Usage:
294 # from transformers import AutoModelForCausalLM
295 # hf = AutoModelForCausalLM.from_pretrained(
296 # "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", torch_dtype=torch.float32)
297 # model.load_hf_state_dict(hf.state_dict())
298 # or load safetensors shards directly and merge them into one dict.
299 # No RoPE permutation is needed: this model now uses the same
300 # half-split rotation as HF ([DS-3]).
301 # ------------------------------------------------------------------
302 @torch.no_grad()
303 def load_hf_state_dict(self, hf_sd: dict, strict: bool = True):
304 mapped = {}
305 mapped["token_emb.weight"] = hf_sd["model.embed_tokens.weight"]
306 mapped["ln_f.weight"] = hf_sd["model.norm.weight"]
307 if "lm_head.weight" in hf_sd:
308 mapped["lm_head.weight"] = hf_sd["lm_head.weight"]
309 else:
310 # fallback for third-party re-uploads that strip lm_head
311 # (official 1.5B distill DOES ship lm_head.weight — untied)
312 mapped["lm_head.weight"] = hf_sd["model.embed_tokens.weight"]
313
314 for i in range(self.config.num_hidden_layers):
315 hf = f"model.layers.{i}"
316 jr = f"blocks.{i}"
317 mapped[f"{jr}.norm1.weight"] = hf_sd[f"{hf}.input_layernorm.weight"]
318 mapped[f"{jr}.norm2.weight"] = hf_sd[f"{hf}.post_attention_layernorm.weight"]
319
320 mapped[f"{jr}.q_proj.weight"] = hf_sd[f"{hf}.self_attn.q_proj.weight"]
321 mapped[f"{jr}.k_proj.weight"] = hf_sd[f"{hf}.self_attn.k_proj.weight"]
322 mapped[f"{jr}.v_proj.weight"] = hf_sd[f"{hf}.self_attn.v_proj.weight"]
323 mapped[f"{jr}.q_proj.bias"] = hf_sd[f"{hf}.self_attn.q_proj.bias"]
324 mapped[f"{jr}.k_proj.bias"] = hf_sd[f"{hf}.self_attn.k_proj.bias"]
325 mapped[f"{jr}.v_proj.bias"] = hf_sd[f"{hf}.self_attn.v_proj.bias"]
326 mapped[f"{jr}.out_proj.weight"] = hf_sd[f"{hf}.self_attn.o_proj.weight"]
327
328 mapped[f"{jr}.ffn_w1.weight"] = hf_sd[f"{hf}.mlp.gate_proj.weight"]
329 mapped[f"{jr}.ffn_w3.weight"] = hf_sd[f"{hf}.mlp.up_proj.weight"]
330 mapped[f"{jr}.ffn_w2.weight"] = hf_sd[f"{hf}.mlp.down_proj.weight"]
331
332 missing, unexpected = self.load_state_dict(mapped, strict=False)
333 # lambda_ buffers are OURS (not in HF) — they legitimately stay missing.
334 real_missing = [k for k in missing if not k.endswith("lambda_")]
335 if strict:
336 assert not real_missing, f"missing from HF checkpoint: {real_missing}"
337 assert not unexpected, f"unexpected keys: {unexpected}"
338 print(f"✅ HF weights loaded: {len(mapped)} tensors "
339 f"({len(real_missing)} missing, {len(unexpected)} unexpected)")
340 return real_missing, unexpected
341
342 # ------------------------------------------------------------------
343 # [FIX-9] Ternary export (biases included, full precision, [DS-5]).
344 # ------------------------------------------------------------------
345 @torch.no_grad()
346 def export_ternary_state_dict(self):
347 out = {}
348 for name, module in self.named_modules():
349 if isinstance(module, BitLinear):
350 w = module.weight.float()
351 gamma = w.abs().mean().clamp(min=module.eps)
352 codes = torch.clamp(torch.round(w / gamma), -1.0, 1.0).to(torch.int8)
353 out[f"{name}.codes"] = codes
354 out[f"{name}.gamma"] = gamma
355 if module.bias is not None:
356 out[f"{name}.bias"] = module.bias.detach().clone()
357 out["token_emb.weight"] = self.token_emb.weight.detach().clone()
358 out["lm_head.weight"] = self.lm_head.weight.detach().clone()
359 out["ln_f.weight"] = self.ln_f.weight.detach().clone()
360 for name, module in self.named_modules():
361 if isinstance(module, RMSNorm) and name != "ln_f":
362 out[f"{name}.weight"] = module.weight.detach().clone()
363 return out
364
365
366 # Convenience aliases so existing training scripts barely change:
367 JiRackConfig = JiRackConfig # drop-in name compat (optional)
368 JiRackTransformer = JiRackTransformer
369 JiRackConfig = JiRackConfig # lets ds7b-style imports work
370 JiRackTransformer = JiRackTransformer
371
372
373 # =============================================================================
374 # Smoke test: python JiRackTernaryPyTorch_ds1p5b.py (tiny config, CPU, seconds)
375 # =============================================================================
376 if __name__ == "__main__":
377 class TinyConfig(JiRackConfig):
378 def __init__(self):
379 super().__init__()
380 self.vocab_size = 256
381 self.hidden_size = 64
382 self.intermediate_size = 128
383 self.num_hidden_layers = 2
384 self.num_attention_heads = 4
385 self.num_key_value_heads = 2
386 self.head_dim = 16
387 self.max_seq_len = 64
388
389 torch.manual_seed(0)
390 model = JiRackTransformer(TinyConfig()).eval()
391 ids = torch.randint(0, 256, (2, 32))
392
393 with torch.no_grad():
394 model.set_lambda(0.0)
395 y0 = model(ids)
396 model.set_lambda(1e-4)
397 y_eps = model(ids)
398 model.set_lambda(1.0)
399 y1 = model(ids)
400
401 # 1) Continuity in lambda.
402 rel_jump = (y_eps - y0).norm() / y0.norm()
403 print(f"relative change at lambda=1e-4: {rel_jump.item():.2e} (must be ~1e-4)")
404 assert rel_jump < 1e-2, "lambda warmup is not continuous!"
405
406 # 2) Output scale sanity at full quantization.
407 ratio = y1.std() / y0.std()
408 print(f"std ratio lambda=1 vs lambda=0: {ratio.item():.3f} (must be O(1))")
409 assert 0.1 < ratio.item() < 10.0, "output scale collapsed or exploded!"
410
411 # 3) Gradients flow through STE at lambda=1 (weights AND qkv biases).
412 model.train()
413 model.set_lambda(1.0)
414 loss = model(ids).float().pow(2).mean()
415 loss.backward()
416 g = model.blocks[0].q_proj.weight.grad
417 gb = model.blocks[0].q_proj.bias.grad
418 assert g is not None and torch.isfinite(g).all() and g.abs().sum() > 0
419 assert gb is not None and torch.isfinite(gb).all(), "qkv bias got no grad!"
420 print(f"grad norms q_proj: weight={g.norm().item():.4f}, bias={gb.norm().item():.4f}")
421
422 # 4) lambda survives a state_dict round-trip.
423 sd = model.state_dict()
424 model2 = JiRackTransformerDS1p5B(TinyConfig())
425 model2.load_state_dict(sd)
426 assert abs(model2.get_lambda() - 1.0) < 1e-9, "lambda_ not serialized!"
427 print("lambda serialization: OK")
428
429 # 5) HF name mapping round-trip on the tiny config: build a fake HF
430 # dict from our own weights, load it back, outputs must match.
431 fake_hf = {
432 "model.embed_tokens.weight": model.token_emb.weight.detach().clone(),
433 "model.norm.weight": model.ln_f.weight.detach().clone(),
434 "lm_head.weight": model.lm_head.weight.detach().clone(),
435 }
436 for i, blk in enumerate(model.blocks):
437 p = f"model.layers.{i}"
438 fake_hf[f"{p}.input_layernorm.weight"] = blk.norm1.weight.detach().clone()
439 fake_hf[f"{p}.post_attention_layernorm.weight"] = blk.norm2.weight.detach().clone()
440 fake_hf[f"{p}.self_attn.q_proj.weight"] = blk.q_proj.weight.detach().clone()
441 fake_hf[f"{p}.self_attn.k_proj.weight"] = blk.k_proj.weight.detach().clone()
442 fake_hf[f"{p}.self_attn.v_proj.weight"] = blk.v_proj.weight.detach().clone()
443 fake_hf[f"{p}.self_attn.q_proj.bias"] = blk.q_proj.bias.detach().clone()
444 fake_hf[f"{p}.self_attn.k_proj.bias"] = blk.k_proj.bias.detach().clone()
445 fake_hf[f"{p}.self_attn.v_proj.bias"] = blk.v_proj.bias.detach().clone()
446 fake_hf[f"{p}.self_attn.o_proj.weight"] = blk.out_proj.weight.detach().clone()
447 fake_hf[f"{p}.mlp.gate_proj.weight"] = blk.ffn_w1.weight.detach().clone()
448 fake_hf[f"{p}.mlp.up_proj.weight"] = blk.ffn_w3.weight.detach().clone()
449 fake_hf[f"{p}.mlp.down_proj.weight"] = blk.ffn_w2.weight.detach().clone()
450
451 model3 = JiRackTransformer(TinyConfig()).eval()
452 model3.load_hf_state_dict(fake_hf)
453 model3.set_lambda(0.0)
454 model.eval(); model.set_lambda(0.0)
455 with torch.no_grad():
456 y_ref = model(ids)
457 y_map = model3(ids)
458 assert torch.allclose(y_ref, y_map, atol=1e-5), "HF mapping mismatch!"
459 print("HF name-mapping round-trip: OK")
460
461 # 6) Export produces genuinely ternary codes + preserved biases.
462 exported = model.export_ternary_state_dict()
463 codes = exported["blocks.0.q_proj.codes"]
464 assert set(codes.unique().tolist()) <= {-1, 0, 1}
465 assert "blocks.0.q_proj.bias" in exported, "qkv bias lost in export!"
466 print(f"export: {sum('codes' in k for k in exported)} ternary tensors, "
467 f"biases preserved, OK")
468
469 print("\nAll smoke tests passed.")
470