File size: 17,693 Bytes
1f76d06 6cac314 1f76d06 | 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 | # =============================================================================
# COPYRIGHT © 2025-2026 Konstantin Vladimirovich Grabko. ALL RIGHTS RESERVED.
# CMS Manhattan JiRack Technology — PATENT PENDING
#
# This code is proprietary.
# Personal and non-commercial research use is allowed.
# Any commercial use, derivative works for profit, or distribution
# requires a paid license and 5% royalty.
#
# Unauthorized commercial use is strictly prohibited.
# Contact: grabko@cmsmanhattan.com
# =============================================================================
#
# JiRack Ternary Transformer 1B - "bitnet.cpp-compatible" revision
#
# Derived from JiRackTernaryPyTorch_1b.py (CMSManhattan/JiRackTernaryPro_1b).
# Three deliberate changes vs. that file, everything else preserved verbatim
# so the existing trained checkpoint loads 1:1 by name:
#
# [C-1] SubLN added. Two new learnable RMSNorm per block -
# attn_sub_norm (before out_proj) and ffn_sub_norm (before ffn_w2).
# bitnet.cpp's BITNET architecture hard-requires these tensors;
# without them the GGUF will not load. 16 layers x 2 = 32 new tensors.
#
# [C-2] lambda_ warmup knob added. The 1B original had NO lambda at all -
# BitLinear was always fully quantized. A lambda_ buffer per BitLinear
# (16 x 7 = 112) lets the network ease back into quantization while
# the brand-new SubLN weights learn their scales. Registered as a
# persistent buffer so it survives save/load.
#
# [C-3] Activation quantization no longer mean-centers.
# The original did `x_norm = x - x.mean(dim=-1)` before absmax.
# bitnet.cpp's I2_S/TL1/TL2 kernels implement plain absmax with NO
# mean subtraction, so keeping the centering would mean training
# against math the inference engine does not perform. Dropping it is
# a real behavioral change - this is part of why the fine-tune below
# is mandatory, not optional.
#
# RoPE IS UNCHANGED ON PURPOSE. The 1B original uses HF/Llama-style
# half-split rotation (rotate_half + freqs repeated as [f0..f31, f0..f31]).
# The 10B "_fixed" line used interleaved/NeoX instead. They are NOT
# interchangeable - the trained weights encode which one they expect.
# Consequence for export: the 1B converter MUST apply the standard
# convert_hf_to_gguf permute to q_proj/k_proj. The 10B converter must not.
# Do not copy one converter onto the other.
# =============================================================================
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint
# ========================= JIRACK 1B CONFIG CONSTANTS =========================
VOCAB_SIZE = 128256
HIDDEN_SIZE = 2048
INTERMEDIATE_SIZE = 8192
NUM_LAYERS = 16
NUM_HEADS = 32
NUM_KV_HEADS = 8
HEAD_DIM = HIDDEN_SIZE // NUM_HEADS # = 64
MAX_SEQ_LEN = 4096
ROPE_THETA = 500000.0
RMS_EPS = 1e-6 # NOTE: 1e-6 here, the 10B line uses 1e-5
INIT_STD = 0.02
# --- quantization stability, from the original file ---
STABILITY_EPS = 1e-9
INT8_SCALE_TARGET = 127.0
QUANT_EPS = 1e-5 # clamp floor used inside BitLinear
# =============================================================================
def _detect_sdpa_gqa() -> bool:
try:
q = torch.zeros(1, 2, 1, 8)
kv = torch.zeros(1, 1, 1, 8)
F.scaled_dot_product_attention(q, kv, kv, enable_gqa=True)
return True
except TypeError:
return False
except Exception:
return False
_SDPA_HAS_GQA = _detect_sdpa_gqa()
class JiRackConfig1B:
def __init__(self):
self.vocab_size = VOCAB_SIZE
self.hidden_size = HIDDEN_SIZE
self.intermediate_size = INTERMEDIATE_SIZE
self.num_hidden_layers = NUM_LAYERS
self.num_attention_heads = NUM_HEADS
self.num_key_value_heads = NUM_KV_HEADS
self.head_dim = HEAD_DIM
self.max_seq_len = MAX_SEQ_LEN
self.max_position_embeddings = MAX_SEQ_LEN
self.rope_theta = ROPE_THETA
self.rms_norm_eps = RMS_EPS
self.init_std = INIT_STD
# ----------------------------------------------------------------------------
# RoPE - HF / Llama half-split style. IDENTICAL MATH to the 1B original.
# freqs has head_dim/2 = 32 entries; it is tiled to 64 as [f0..f31, f0..f31],
# and rotate_half splits the vector down the middle. This is what
# convert_hf_to_gguf.py's permute() is designed to undo, hence the converter
# for this model applies that permute.
# ----------------------------------------------------------------------------
def precompute_freqs_cis(dim: int, seq_len: int, theta: float = ROPE_THETA):
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
t = torch.arange(seq_len).float()
freqs = torch.outer(t, freqs)
return torch.cos(freqs), torch.sin(freqs)
def apply_rotary_emb(xq, xk, freqs_cos, freqs_sin):
def rotate_half(x):
x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2:]
return torch.cat((-x2, x1), dim=-1)
T = xq.shape[2]
f_cos = freqs_cos[:T].to(device=xq.device, dtype=xq.dtype).view(1, 1, T, -1).repeat(1, 1, 1, 2)
f_sin = freqs_sin[:T].to(device=xq.device, dtype=xq.dtype).view(1, 1, T, -1).repeat(1, 1, 1, 2)
xq_out = (xq * f_cos) + (rotate_half(xq) * f_sin)
xk_out = (xk * f_cos) + (rotate_half(xk) * f_sin)
return xq_out, xk_out
class BitLinear(nn.Linear):
"""Canonical BitNet b1.58 fake-quant linear.
No internal normalization - that is the caller's job (norm1/norm2 for the
pre-norms, attn_sub_norm/ffn_sub_norm for SubLN). Weight quant is absmean
ternary; activation quant is plain absmax int8, NO mean centering, see
[C-3] in the header.
"""
def __init__(self, in_features, out_features, bias=False):
super().__init__(in_features, out_features, bias=bias)
self.eps = QUANT_EPS
# [C-2] persistent so it round-trips through save/load
self.register_buffer("lambda_", torch.zeros(()), persistent=True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if not self.training and float(self.lambda_) < 1e-6:
return F.linear(x, self.weight, self.bias)
lam = self.lambda_.to(x.dtype)
w = self.weight
gamma = w.float().abs().mean().clamp(min=self.eps).to(w.dtype)
w_quant = torch.clamp(torch.round(w / gamma), -1.0, 1.0) * gamma
w_effective = w + lam * (w_quant - w).detach()
x_scale = INT8_SCALE_TARGET / x.abs().max(dim=-1, keepdim=True).values.clamp(min=self.eps)
x_quant = torch.clamp(torch.round(x * x_scale), -128.0, 127.0) / x_scale
x_effective = x + lam * (x_quant - x).detach()
return F.linear(x_effective, w_effective, self.bias)
class RMSNorm(nn.Module):
"""Learnable RMSNorm. Used for the block pre-norms AND for SubLN -
same math, different position in the graph."""
def __init__(self, dim, eps=RMS_EPS):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
dtype = x.dtype
x = x.float()
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
return (x * self.weight.float()).to(dtype)
def repeat_kv(x, n_rep):
if n_rep == 1:
return x
return x.repeat_interleave(n_rep, dim=1)
class TransformerBlock(nn.Module):
def __init__(self, config, use_checkpoint=False, bias=False):
super().__init__()
self.use_checkpoint = use_checkpoint
self.n_heads = config.num_attention_heads
self.n_kv_heads = config.num_key_value_heads
self.head_dim = config.head_dim
self.n_rep = self.n_heads // self.n_kv_heads
self.q_proj = BitLinear(config.hidden_size, config.hidden_size, bias=bias)
self.k_proj = BitLinear(config.hidden_size, self.n_kv_heads * self.head_dim, bias=bias)
self.v_proj = BitLinear(config.hidden_size, self.n_kv_heads * self.head_dim, bias=bias)
self.out_proj = BitLinear(config.hidden_size, config.hidden_size, bias=bias)
self.ffn_w1 = BitLinear(config.hidden_size, config.intermediate_size, bias=bias)
self.ffn_w3 = BitLinear(config.hidden_size, config.intermediate_size, bias=bias)
self.ffn_w2 = BitLinear(config.intermediate_size, config.hidden_size, bias=bias)
self.norm1 = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.norm2 = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
# [C-1] SubLN, exactly where bitnet.cpp expects it
self.attn_sub_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.ffn_sub_norm = RMSNorm(config.intermediate_size, eps=config.rms_norm_eps)
def forward(self, x, freqs_cos, freqs_sin):
if self.use_checkpoint and self.training:
return checkpoint(self._forward_impl, x, freqs_cos, freqs_sin, use_reentrant=False)
return self._forward_impl(x, freqs_cos, freqs_sin)
def _forward_impl(self, x, freqs_cos, freqs_sin):
h = self.norm1(x)
B, T, D = x.shape
q = self.q_proj(h).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
k = self.k_proj(h).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
v = self.v_proj(h).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
q, k = apply_rotary_emb(q, k, freqs_cos, freqs_sin)
if self.n_rep > 1 and _SDPA_HAS_GQA:
attn_out = F.scaled_dot_product_attention(q, k, v, is_causal=True, enable_gqa=True)
else:
k, v = repeat_kv(k, self.n_rep), repeat_kv(v, self.n_rep)
attn_out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
attn_out = attn_out.transpose(1, 2).contiguous().view(B, T, D)
# [C-1] SubLN before the ternary output projection
attn_out = self.attn_sub_norm(attn_out)
x = x + self.out_proj(attn_out)
m = self.norm2(x)
gate = F.silu(self.ffn_w1(m))
up = self.ffn_w3(m)
ffn_hidden = gate * up
# [C-1] SubLN before the ternary down projection
ffn_hidden = self.ffn_sub_norm(ffn_hidden)
x = x + self.ffn_w2(ffn_hidden)
return x
class JiRackTransformer1B(nn.Module):
def __init__(self, config: JiRackConfig1B = None, use_checkpoint=False, bias=False):
super().__init__()
self.config = config if config is not None else JiRackConfig1B()
self.use_checkpoint = use_checkpoint
self.token_emb = nn.Embedding(self.config.vocab_size, self.config.hidden_size)
self.blocks = nn.ModuleList([
TransformerBlock(self.config, self.use_checkpoint, bias=bias)
for _ in range(self.config.num_hidden_layers)
])
self.ln_f = RMSNorm(self.config.hidden_size, eps=self.config.rms_norm_eps)
self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False)
cos, sin = precompute_freqs_cis(
dim=self.config.head_dim,
seq_len=self.config.max_seq_len,
theta=self.config.rope_theta,
)
# NOTE: the 1B original registered these as PERSISTENT buffers, so an
# old checkpoint may or may not carry freqs_cos/freqs_sin depending on
# how it was saved. Non-persistent here (they are recomputed at init);
# the migration script tolerates them being present in the old file.
self.register_buffer("freqs_cos", cos, persistent=False)
self.register_buffer("freqs_sin", sin, persistent=False)
self._init_weights()
def _init_weights(self):
std = self.config.init_std
resid_std = std / math.sqrt(2 * self.config.num_hidden_layers)
nn.init.normal_(self.token_emb.weight, mean=0.0, std=std)
nn.init.normal_(self.lm_head.weight, mean=0.0, std=std)
for block in self.blocks:
for lin in (block.q_proj, block.k_proj, block.v_proj, block.ffn_w1, block.ffn_w3):
nn.init.normal_(lin.weight, mean=0.0, std=std)
if lin.bias is not None:
nn.init.zeros_(lin.bias)
for lin in (block.out_proj, block.ffn_w2):
nn.init.normal_(lin.weight, mean=0.0, std=resid_std)
if lin.bias is not None:
nn.init.zeros_(lin.bias)
# SubLN weights stay at their RMSNorm init of 1.0 -> near-identity,
# which minimizes the shock of the architecture change.
def load_from_old_checkpoint(self, old_state_dict: dict):
"""Load weights from the ORIGINAL 1B architecture (no SubLN, no lambda_).
Tensor names are identical between the two files, so this is a plain
strict=False load. Exactly three categories of difference are tolerated:
1. missing *.sub_norm.weight - the 32 new SubLN tensors [C-1].
Left at init 1.0, must be learned during the fine-tune.
2. missing *.lambda_ - the 112 new warmup buffers [C-2].
Left at 0.0, the correct start for a fresh warmup.
3. unexpected freqs_cos/freqs_sin - the original registered its RoPE
tables as persistent buffers, so they may be baked into the old
file. They are recomputed at init here, so they are ignored.
Anything else is a real mismatch and raises.
"""
missing, unexpected = self.load_state_dict(old_state_dict, strict=False)
new_subln = [m for m in missing if "sub_norm" in m]
new_lambda = [m for m in missing if m.endswith("lambda_")]
other_missing = [m for m in missing
if "sub_norm" not in m and not m.endswith("lambda_")]
rope_extra = [u for u in unexpected if u in ("freqs_cos", "freqs_sin")]
other_unexpected = [u for u in unexpected if u not in ("freqs_cos", "freqs_sin")]
if other_missing:
raise RuntimeError(
f"{len(other_missing)} unexpected missing keys beyond SubLN/lambda_: "
f"{other_missing[:10]}"
)
if other_unexpected:
raise RuntimeError(
f"{len(other_unexpected)} unexpected keys in old checkpoint: "
f"{other_unexpected[:10]}"
)
n_layers = self.config.num_hidden_layers
print("Loaded old 1B checkpoint.")
print(f" {len(new_subln)} SubLN tensors initialized at 1.0 "
f"(expected {2 * n_layers}), e.g. {new_subln[:2]}")
print(f" {len(new_lambda)} lambda_ buffers initialized at 0.0 "
f"(expected {7 * n_layers}) - warmup starts fresh")
if rope_extra:
print(f" ignored baked-in RoPE tables: {rope_extra}")
def set_lambda(self, lambda_value: float):
for module in self.modules():
if isinstance(module, BitLinear):
module.lambda_.fill_(lambda_value)
def get_lambda(self) -> float:
for module in self.modules():
if isinstance(module, BitLinear):
return float(module.lambda_)
return 0.0
def forward(self, input_ids):
x = self.token_emb(input_ids)
for block in self.blocks:
x = block(x, self.freqs_cos, self.freqs_sin)
return self.lm_head(self.ln_f(x))
@torch.no_grad()
def export_ternary_state_dict(self):
"""Bake fake-quant weights into real ternary codes + per-tensor scale.
SubLN weights are full precision and pass through untouched, same as
embeddings / lm_head / norms."""
out = {}
for name, module in self.named_modules():
if isinstance(module, BitLinear):
w = module.weight.float()
gamma = w.abs().mean().clamp(min=module.eps)
codes = torch.clamp(torch.round(w / gamma), -1.0, 1.0).to(torch.int8)
out[f"{name}.codes"] = codes
out[f"{name}.gamma"] = gamma
if module.bias is not None:
out[f"{name}.bias"] = module.bias.detach().clone()
out["token_emb.weight"] = self.token_emb.weight.detach().clone()
out["lm_head.weight"] = self.lm_head.weight.detach().clone()
out["ln_f.weight"] = self.ln_f.weight.detach().clone()
for name, module in self.named_modules():
if isinstance(module, RMSNorm) and name != "ln_f":
out[f"{name}.weight"] = module.weight.detach().clone()
return out
if __name__ == "__main__":
class TinyConfig(JiRackConfig1B):
def __init__(self):
super().__init__()
self.vocab_size = 256
self.hidden_size = 64
self.intermediate_size = 128
self.num_hidden_layers = 2
self.num_attention_heads = 4
self.num_key_value_heads = 2
self.head_dim = 16
self.max_seq_len = 64
torch.manual_seed(0)
model = JiRackTransformer1B(TinyConfig()).eval()
ids = torch.randint(0, 256, (2, 32))
with torch.no_grad():
model.set_lambda(0.0)
y0 = model(ids)
model.set_lambda(1.0)
y1 = model(ids)
assert torch.isfinite(y0).all() and torch.isfinite(y1).all()
ratio = y1.std() / y0.std()
print(f"std ratio lambda=1 vs lambda=0: {ratio.item():.3f} (should be O(1))")
sd = model.state_dict()
subln = [n for n in sd if "sub_norm" in n]
lam = [n for n in sd if n.endswith("lambda_")]
print(f"SubLN tensors: {len(subln)} (expect 2 x n_layers)")
print(f"lambda_ buffers: {len(lam)} (expect 7 x n_layers)")
print("Smoke test OK.")
|