JiRackTernaryPro_1b / bitnet.cpp /train_bitnet_lambda_warmup_1b.py
kgrabko's picture
Update bitnet.cpp/train_bitnet_lambda_warmup_1b.py
9b4f97f verified
Raw History Blame Contribute Delete
15 kB
%%writefile train_jirack_bitnet_1b_lambda_warmup.py
# =============================================================================
# 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
# =============================================================================
# train_jirack_bitnet_1b_lambda_warmup.py
# =============================================================================
# QAT training with lambda warmup β€” for the SubLN-equipped, bitnet.cpp-
# compatible architecture (JiRackTernaryPyTorch_1b_bitnet.py).
#
# Same warmup machinery as the 10B script, re-pointed at the 1B model.
# Differences from the 10B version, all deliberate:
#
# [B-1] import from JiRackTernaryPyTorch_1b_bitnet (SubLN architecture).
# [B-2] BASE_CHECKPOINT points at the output of
# load_old_checkpoint_into_bitnet_arch_1b.py β€” the migrated
# checkpoint where SubLN weights are freshly initialized at 1.0.
# [B-3] BATCH_SIZE raised 2 -> 8 and GRAD_ACCUM lowered 5 -> 2. The 1B
# model is ~10x smaller, so on a 96GB card there is no reason to
# crawl at micro-batch 2. Effective batch stays 16 (was 10).
# [B-4] LAMBDA_WARMUP_STEPS 5000 -> 2000 micro-batches. The 1B weights
# are ALREADY fully ternary-trained (the original had no lambda at
# all β€” it ran quantized from step one), so this warmup only has to
# settle the new SubLN weights, not re-ternarize from scratch.
#
# Everything else β€” the sigmoid lambda schedule, resume-by-global-step,
# optimizer state save/restore, lambda-per-accumulation-window, lambda-
# tagged validation β€” is unchanged.
#
# WHAT TO WATCH, 1B-specific: this run changes TWO things at once, SubLN
# appearing and activation quantization dropping its mean-centering ([C-3]
# in the architecture file). So expect a real loss spike as lambda crosses
# the steep middle of the sigmoid. Judge by val loss @ a given lambda
# trending down shard over shard. If it has not recovered by the end of
# warmup, raise LAMBDA_WARMUP_STEPS (3000-4000) rather than touching LR β€”
# the sigmoid midpoint is the sensitive part, not the learning rate.
# =============================================================================
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import torch
import glob
import re
import gc
import math
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torch.nn.utils.rnn import pad_sequence
from tqdm import tqdm
from transformers.optimization import Adafactor
# [B-1] SubLN / bitnet.cpp-compatible model class
from JiRackTernaryPyTorch_1b_bitnet import JiRackTransformer1B, JiRackConfig1B
# ========================= ΠΠΠ‘Π’Π ΠžΠ™ΠšΠ˜ =========================
DATA_DIR = "/content/prepared_sft_data"
OUTPUT_DIR = "/content/JRock_1B_Bitnet_Checkpoints"
# [B-2] output of load_old_checkpoint_into_bitnet_arch_1b.py
BASE_CHECKPOINT = "/content/jirack_1b_bitnet_init.pt"
BATCH_SIZE = 8 # [B-3] 1B fits comfortably; raise further if VRAM allows
GRAD_ACCUM = 2 # [B-3] effective batch = 16
LR = 4e-5
VAL_RATIO = 0.05
# === Lambda Warmup Settings ===
# [B-4] 2000 micro-batches = 1000 optimizer steps @ GRAD_ACCUM=2.
# Shorter than the 10B run on purpose: the 1B weights are already ternary,
# this warmup only has to settle the new SubLN. If val loss has not
# recovered by the end of warmup, raise this (3000-4000), not LR.
LAMBDA_WARMUP_STEPS = 2000 # in MICRO-batches
MAX_LAMBDA = 1.0
SAVE_OPTIMIZER = True
AUTOSAVE_EVERY = 1000
# =============================================================
os.makedirs(OUTPUT_DIR, exist_ok=True)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
print("πŸš€ Loading JiRack 1B Ternary (SubLN / bitnet.cpp) + Lambda Warmup...")
config = JiRackConfig1B()
# [B-3] gradient checkpointing OFF for 1B β€” it trades ~30% speed for memory
# you do not need at this size. Flip to True if you raise BATCH_SIZE a lot.
USE_GRAD_CHECKPOINT = False
model = JiRackTransformer1B(config, use_checkpoint=USE_GRAD_CHECKPOINT)
model.to("cuda")
for block in model.blocks:
block.use_checkpoint = USE_GRAD_CHECKPOINT
print(f"βœ… Gradient checkpointing: {'enabled' if USE_GRAD_CHECKPOINT else 'disabled'}")
optimizer = Adafactor(
model.parameters(),
lr=LR,
eps=(1e-30, 1e-3),
clip_threshold=1.0,
decay_rate=-0.8,
weight_decay=0.0001,
scale_parameter=False,
relative_step=False,
warmup_init=False,
)
criterion = nn.CrossEntropyLoss(ignore_index=-100)
# ==================== LAMBDA SCHEDULE ====================
def lambda_schedule(step: int) -> float:
"""Sigmoid ramp over LAMBDA_WARMUP_STEPS micro-batches."""
if step >= LAMBDA_WARMUP_STEPS:
return MAX_LAMBDA
return MAX_LAMBDA / (
1.0 + math.exp(-10 * (step - LAMBDA_WARMUP_STEPS / 2) / LAMBDA_WARMUP_STEPS)
)
def invert_lambda_schedule(lam: float) -> int:
"""Reconstruct global_step from a saved lambda (legacy checkpoints
that stored only the model state_dict). Inverse of the sigmoid above."""
if lam >= MAX_LAMBDA * 0.999:
return LAMBDA_WARMUP_STEPS
if lam <= 1e-6:
return 0
p = lam / MAX_LAMBDA
x = -math.log(1.0 / p - 1.0) # logit
return int(round(x * LAMBDA_WARMUP_STEPS / 10 + LAMBDA_WARMUP_STEPS / 2))
# ==================== CHECKPOINT LOAD ====================
def load_any_checkpoint(path, model, optimizer):
"""Handles both new-format dicts and legacy plain state_dicts.
Returns restored global_step.
NOTE: on the very first load (BASE_CHECKPOINT = migration output),
the state_dict has EVERY tensor including attn_sub_norm/ffn_sub_norm
(the migration script already wrote them, at value 1.0), so this is
a normal strict-ish load β€” no special-casing needed here for SubLN."""
ckpt = torch.load(path, map_location="cpu", weights_only=True)
if isinstance(ckpt, dict) and "model" in ckpt:
missing, unexpected = model.load_state_dict(ckpt["model"], strict=False)
assert not unexpected, unexpected
assert all(k.endswith("lambda_") for k in missing), missing
if SAVE_OPTIMIZER and "optimizer" in ckpt and ckpt["optimizer"] is not None:
try:
optimizer.load_state_dict(ckpt["optimizer"])
print("βœ… Optimizer state restored")
except Exception as e:
print(f"⚠️ Optimizer state not restored ({e}); continuing fresh")
step = int(ckpt.get("global_step", 0))
print(f"βœ… Resumed (new format): global_step={step}, "
f"lambda={ckpt.get('lambda', 'n/a')}")
return step
# legacy: plain state_dict (e.g. straight from load_from_old_checkpoint
# + torch.save, no wrapper dict) β€” missing keys should only be lambda_
# buffers here, since the migration script already fills SubLN weights.
missing, unexpected = model.load_state_dict(ckpt, strict=False)
assert not unexpected, unexpected
assert all(k.endswith("lambda_") for k in missing), missing
lam = model.get_lambda()
step = invert_lambda_schedule(lam)
print(f"βœ… Resumed (legacy format): lambda={lam:.4f} -> "
f"reconstructed global_step={step}")
return step
checkpoints = sorted(
glob.glob(os.path.join(OUTPUT_DIR, "jirack_1b_data_*.pt")),
key=lambda x: int(re.search(r"data_(\d+)", x).group(1)),
)
all_ckpts = glob.glob(os.path.join(OUTPUT_DIR, "*.pt"))
global_step = 0
if all_ckpts:
LATEST_CKPT = max(all_ckpts, key=os.path.getmtime)
print(f"πŸ“¦ Resuming from (newest on disk): {LATEST_CKPT}")
global_step = load_any_checkpoint(LATEST_CKPT, model, optimizer)
else:
print(f"πŸ“¦ Loading base (migrated) checkpoint: {BASE_CHECKPOINT}")
assert os.path.exists(BASE_CHECKPOINT), (
f"{BASE_CHECKPOINT} not found β€” run "
f"load_old_checkpoint_into_bitnet_arch_1b.py first"
)
global_step = load_any_checkpoint(BASE_CHECKPOINT, model, optimizer)
# Fresh migration -> starting lambda warmup from scratch, even if the
# checkpoint happened to carry a lambda value from the old run.
global_step = 0
print("πŸ”„ Fresh SubLN migration detected: lambda warmup restarted from step 0 "
"(SubLN has never seen a nonzero lambda before).")
model.set_lambda(lambda_schedule(global_step))
print(f"πŸ”§ Warmup: {LAMBDA_WARMUP_STEPS} steps | starting at "
f"step={global_step}, lambda={model.get_lambda():.4f}")
# ========================= DATASET =========================
class ShardDataset(Dataset):
def __init__(self, data_list):
self.data = data_list
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx]
def collate_fn(batch):
input_ids = pad_sequence(
[item["input_ids"] for item in batch], batch_first=True, padding_value=0
)
attention_mask = pad_sequence(
[item.get("attention_mask", torch.ones_like(item["input_ids"]))
for item in batch],
batch_first=True, padding_value=0,
)
labels = input_ids.clone()
labels[attention_mask == 0] = -100
return {"input_ids": input_ids, "labels": labels}
# ========================= CHECKPOINT SAVE =========================
def save_checkpoint(path, model, optimizer, global_step, lam):
model_sd = {k: v.detach().to(torch.bfloat16).cpu()
for k, v in model.state_dict().items()}
ckpt = {
"model": model_sd,
"optimizer": optimizer.state_dict() if SAVE_OPTIMIZER else None,
"global_step": global_step,
"lambda": lam,
}
torch.save(ckpt, path)
# ========================= TRAINING =========================
all_shards = sorted(
glob.glob(f"{DATA_DIR}/sft_data_*.pt"),
key=lambda x: int(re.search(r"sft_data_(\d+)", x).group(1)),
)
last_done = -1
if checkpoints:
m = re.search(r"data_(\d+)", checkpoints[-1])
last_done = int(m.group(1)) if m else -1
for shard_path in all_shards:
shard_name = os.path.basename(shard_path)
shard_num = int(re.search(r"sft_data_(\d+)", shard_name).group(1))
if shard_num <= last_done:
print(f"⏭ Skipping already processed: {shard_name}")
continue
print(f"\nπŸ”₯ Starting shard: {shard_name}")
raw_shard_data = torch.load(shard_path, map_location="cpu", weights_only=False)
val_size = int(len(raw_shard_data) * VAL_RATIO)
train_size = len(raw_shard_data) - val_size
train_data, val_data = torch.utils.data.random_split(
raw_shard_data, [train_size, val_size],
generator=torch.Generator().manual_seed(42),
)
train_loader = DataLoader(
ShardDataset(train_data), batch_size=BATCH_SIZE, shuffle=True,
collate_fn=collate_fn, pin_memory=True,
)
model.train()
pbar = tqdm(train_loader, desc=f"Shard {shard_num}", dynamic_ncols=True)
optimizer.zero_grad()
lambda_value = lambda_schedule(global_step)
model.set_lambda(lambda_value)
for step, batch in enumerate(pbar):
if step % GRAD_ACCUM == 0:
lambda_value = lambda_schedule(global_step)
model.set_lambda(lambda_value)
input_ids = batch["input_ids"].to("cuda", non_blocking=True)
labels = batch["labels"].to("cuda", non_blocking=True)
with torch.amp.autocast("cuda", dtype=torch.bfloat16):
logits = model(input_ids)
if isinstance(logits, tuple):
logits = logits[0]
loss = criterion(
logits[..., :-1, :].reshape(-1, config.vocab_size),
labels[..., 1:].reshape(-1),
)
loss = loss / GRAD_ACCUM
if torch.isnan(loss) or torch.isinf(loss):
print(f"\n⚠️ NaN/Inf loss at step {global_step} "
f"(lambda={lambda_value:.4f}) β€” window dropped")
optimizer.zero_grad(set_to_none=True)
torch.cuda.empty_cache()
global_step += 1
continue
loss.backward()
if (step + 1) % GRAD_ACCUM == 0:
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
optimizer.zero_grad()
if step % 10 == 0:
pbar.set_postfix({
"loss": f"{loss.item() * GRAD_ACCUM:.4f}",
"lambda": f"{lambda_value:.4f}",
"gstep": global_step,
})
if (AUTOSAVE_EVERY > 0 and global_step > 0
and global_step % AUTOSAVE_EVERY == 0
and (step + 1) % GRAD_ACCUM == 0):
autosave_path = os.path.join(OUTPUT_DIR, "autosave_latest.pt")
tmp_path = autosave_path + ".tmp"
save_checkpoint(tmp_path, model, optimizer, global_step, lambda_value)
os.replace(tmp_path, autosave_path)
pbar.write(f"πŸ’Ύ autosave @ gstep={global_step}, "
f"lambda={lambda_value:.4f}")
global_step += 1
# ==================== Validation ====================
print("πŸ§ͺ Validating...")
model.eval()
total_val_loss = 0.0
val_steps = 0
val_loader = DataLoader(
ShardDataset(val_data), batch_size=BATCH_SIZE, shuffle=False,
collate_fn=collate_fn, pin_memory=True,
)
with torch.no_grad():
for batch in tqdm(val_loader, desc="Validating", leave=False):
input_ids = batch["input_ids"].to("cuda", non_blocking=True)
labels = batch["labels"].to("cuda", non_blocking=True)
with torch.amp.autocast("cuda", dtype=torch.bfloat16):
logits = model(input_ids)
if isinstance(logits, tuple):
logits = logits[0]
v_loss = criterion(
logits[..., :-1, :].reshape(-1, config.vocab_size),
labels[..., 1:].reshape(-1),
)
if not (torch.isnan(v_loss) or torch.isinf(v_loss)):
total_val_loss += v_loss.item()
val_steps += 1
avg_val_loss = total_val_loss / val_steps if val_steps > 0 else float("inf")
print(f"πŸ“Š Shard {shard_num} β€” Val Loss: {avg_val_loss:.4f} "
f"@ lambda={lambda_value:.4f} (gstep={global_step})")
save_path = os.path.join(OUTPUT_DIR, f"jirack_1b_data_{shard_num}.pt")
save_checkpoint(save_path, model, optimizer, global_step, lambda_value)
print(f"πŸ’Ύ Saved: {save_path}")
del raw_shard_data
torch.cuda.empty_cache()
gc.collect()
print("🏁 Training finished with Lambda Warmup (SubLN / bitnet.cpp arch)!")