Update code/train_sft.py with user enhancements
Browse files- code/train_sft.py +71 -25
code/train_sft.py
CHANGED
|
@@ -74,7 +74,9 @@ if cur_dir not in sys.path:
|
|
| 74 |
from model import ViuAI, Transformer
|
| 75 |
from config import ViuAIConfig, ModelArgs
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
|
| 79 |
DOMAIN_NAMES_V18 = {
|
| 80 |
0: "gk_polity_history",
|
|
@@ -308,10 +310,11 @@ def get_lr(it, warmup_steps, total_steps, max_lr, min_lr):
|
|
| 308 |
|
| 309 |
|
| 310 |
|
| 311 |
-
|
|
|
|
| 312 |
|
| 313 |
def ensure_cloud_data_and_checkpoint(version: str, data_dir: str, ckpt_path: str, token: str = None):
|
| 314 |
-
token = token or os.environ.get("HF_TOKEN")
|
| 315 |
|
| 316 |
if str(version).lower() == "viutranslate":
|
| 317 |
stage_subfolder = "ViuTranslate"
|
|
@@ -404,7 +407,9 @@ def run_live_eval_previews(model, tokenizer, device, version=""):
|
|
| 404 |
if tokenizer is None:
|
| 405 |
return
|
| 406 |
|
| 407 |
-
|
|
|
|
|
|
|
| 408 |
test_suite = [
|
| 409 |
("Identity", "<|user|>\nWho created you and what is your purpose?<|endofturn|>\n<|assistant|>\n", 40),
|
| 410 |
("EN -> HI Direct", "<|user|>\nThe sun rises in the east and sets in the west.<|endofturn|>\n<|assistant|>\n", 45),
|
|
@@ -412,7 +417,7 @@ def run_live_eval_previews(model, tokenizer, device, version=""):
|
|
| 412 |
("Proverb Command", "<|user|>\nTranslate to Hindi: 'Consistency and discipline are the keys to long term success.'<|endofturn|>\n<|assistant|>\n", 50),
|
| 413 |
("Hinglish Command","<|user|>\nTranslate to Hinglish: 'I am waiting outside your office, please call me when you are free.'<|endofturn|>\n<|assistant|>\n", 50)
|
| 414 |
]
|
| 415 |
-
elif
|
| 416 |
test_suite = [
|
| 417 |
("Identity", "<|user|>\nWho created you and what is your name?<|endofturn|>\n<|assistant|>\n", 40),
|
| 418 |
("Indian Greet", "<|user|>\nRam Ram bhai! Kaise ho?<|endofturn|>\n<|assistant|>\n", 35),
|
|
@@ -509,6 +514,10 @@ def main():
|
|
| 509 |
|
| 510 |
ckpt_filename = "viutranslate_final.pt" if str(args.version).lower() == "viutranslate" else f"sft_{args.version}_final.pt"
|
| 511 |
save_path = os.path.join(output_dir, ckpt_filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
if args.init_ckpt:
|
| 514 |
init_ckpt = args.init_ckpt
|
|
@@ -524,16 +533,20 @@ def main():
|
|
| 524 |
tokenizer = None
|
| 525 |
tok_path = os.path.join(root_dir, "tokenizer", "tokenizer.json")
|
| 526 |
global PAD_TOKEN_ID, EOT_ID
|
| 527 |
-
|
|
|
|
| 528 |
EOT_ID = 64002
|
|
|
|
| 529 |
if os.path.exists(tok_path):
|
| 530 |
try:
|
| 531 |
from tokenizers import Tokenizer
|
| 532 |
tokenizer = Tokenizer.from_file(tok_path)
|
| 533 |
v = tokenizer.get_vocab()
|
| 534 |
-
PAD_TOKEN_ID = v.get("<|user|>", 64000)
|
| 535 |
EOT_ID = v.get("<|endofturn|>", 64002)
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
| 537 |
except Exception as e:
|
| 538 |
print(f"โ ๏ธ Could not load tokenizer for special tokens ({e}). Using defaults.")
|
| 539 |
|
|
@@ -553,13 +566,17 @@ def main():
|
|
| 553 |
print(f"โ ๏ธ Notice: Could not parse metadata.json domain_names ({e}). Falling back to version heuristic.")
|
| 554 |
|
| 555 |
if not loaded_dynamic_domains:
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
DOMAIN_NAMES = DOMAIN_NAMES_V2
|
| 558 |
-
elif
|
| 559 |
DOMAIN_NAMES = DOMAIN_NAMES_V1
|
| 560 |
-
elif
|
| 561 |
DOMAIN_NAMES = DOMAIN_NAMES_V22
|
| 562 |
-
elif
|
| 563 |
DOMAIN_NAMES = DOMAIN_NAMES_V19
|
| 564 |
else:
|
| 565 |
DOMAIN_NAMES = DOMAIN_NAMES_V2
|
|
@@ -601,17 +618,34 @@ def main():
|
|
| 601 |
return None
|
| 602 |
return cand1
|
| 603 |
|
| 604 |
-
# 1. Load Data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
train_dataset = SFTDataset(
|
| 606 |
-
ids_path=
|
| 607 |
-
labels_path=
|
| 608 |
-
offsets_path=
|
| 609 |
domains_path=find_shard_path(stage_data_dir, "train_domains", args.version)
|
| 610 |
)
|
| 611 |
val_dataset = SFTDataset(
|
| 612 |
-
ids_path=
|
| 613 |
-
labels_path=
|
| 614 |
-
offsets_path=
|
| 615 |
domains_path=find_shard_path(stage_data_dir, "val_domains", args.version)
|
| 616 |
)
|
| 617 |
|
|
@@ -646,7 +680,7 @@ def main():
|
|
| 646 |
print("๐พ Activation Checkpointing: ENABLED (low-VRAM mode)")
|
| 647 |
|
| 648 |
model_args = ViuAIConfig(
|
| 649 |
-
vocab_size=
|
| 650 |
context_length=2048,
|
| 651 |
z_loss_weight=0.0,
|
| 652 |
attn_dropout=0.05,
|
|
@@ -766,13 +800,20 @@ def main():
|
|
| 766 |
inps = inps.to(device, non_blocking=True)
|
| 767 |
lbls = lbls.to(device, non_blocking=True)
|
| 768 |
with autocast_ctx:
|
| 769 |
-
# Single forward pass: get hidden states
|
| 770 |
B, T = inps.shape
|
| 771 |
x = raw_m.tok_emb(inps)
|
| 772 |
cos = raw_m.rope_cos.to(x.device)
|
| 773 |
sin = raw_m.rope_sin.to(x.device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 774 |
for block in raw_m.blocks:
|
| 775 |
-
x = block(x, cos, sin,
|
| 776 |
x = raw_m.final_norm(x)
|
| 777 |
hidden = x[:, :-1, :].contiguous() # [B, T-1, D]
|
| 778 |
shift_labels = lbls[:, 1:].contiguous() # [B, T-1]
|
|
@@ -855,7 +896,7 @@ def main():
|
|
| 855 |
total_tokens_trained += active_tokens_count
|
| 856 |
|
| 857 |
with autocast_ctx:
|
| 858 |
-
_, loss = model(inputs, targets=labels, pad_id=
|
| 859 |
loss_scaled = loss / grad_acc
|
| 860 |
|
| 861 |
loss_scaled.backward()
|
|
@@ -925,7 +966,10 @@ def main():
|
|
| 925 |
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
| 926 |
}
|
| 927 |
torch.save(save_payload, save_path)
|
| 928 |
-
print(f" ๐พ Saved checkpoint -> {save_path}
|
|
|
|
|
|
|
|
|
|
| 929 |
|
| 930 |
# End of Epoch Handling
|
| 931 |
if epoch_batches > 0 and (micro_idx + 1) % grad_acc != 0:
|
|
@@ -969,11 +1013,13 @@ def main():
|
|
| 969 |
}
|
| 970 |
torch.save(final_payload, save_path)
|
| 971 |
print(f"๐พ Final master checkpoint saved to: {save_path}")
|
|
|
|
|
|
|
| 972 |
|
| 973 |
# Push to Hugging Face Hub (Only Final Checkpoint)
|
| 974 |
if args.push_to_hf:
|
| 975 |
print("\n๐ Pushing Final Checkpoint to Hugging Face Model Hub (ViuAI/ViuAI-500M)...")
|
| 976 |
-
token = args.hf_token or os.environ.get("HF_TOKEN")
|
| 977 |
if token:
|
| 978 |
try:
|
| 979 |
api = HfApi(token=token)
|
|
|
|
| 74 |
from model import ViuAI, Transformer
|
| 75 |
from config import ViuAIConfig, ModelArgs
|
| 76 |
|
| 77 |
+
# PAD = EOT (end-of-turn) used as padding, standard for models without a dedicated
|
| 78 |
+
# pad token. Never use <|user|> (64000) as pad: it pollutes turn-start semantics.
|
| 79 |
+
PAD_TOKEN_ID = 64002
|
| 80 |
|
| 81 |
DOMAIN_NAMES_V18 = {
|
| 82 |
0: "gk_polity_history",
|
|
|
|
| 310 |
|
| 311 |
|
| 312 |
|
| 313 |
+
# SECURITY: HF token must come from environment only. Never hardcode tokens.
|
| 314 |
+
# Set HF_TOKEN env var before running. See README for instructions.
|
| 315 |
|
| 316 |
def ensure_cloud_data_and_checkpoint(version: str, data_dir: str, ckpt_path: str, token: str = None):
|
| 317 |
+
token = token or os.environ.get("HF_TOKEN")
|
| 318 |
|
| 319 |
if str(version).lower() == "viutranslate":
|
| 320 |
stage_subfolder = "ViuTranslate"
|
|
|
|
| 407 |
if tokenizer is None:
|
| 408 |
return
|
| 409 |
|
| 410 |
+
_vnorm = str(version).lower().replace("sft_", "").replace("sft", "").strip()
|
| 411 |
+
_vnum = _vnorm[1:] if _vnorm.startswith("v") and _vnorm[1:].isdigit() else _vnorm
|
| 412 |
+
if _vnorm in ("translat", "viutranslate") or "translat" in _vnorm:
|
| 413 |
test_suite = [
|
| 414 |
("Identity", "<|user|>\nWho created you and what is your purpose?<|endofturn|>\n<|assistant|>\n", 40),
|
| 415 |
("EN -> HI Direct", "<|user|>\nThe sun rises in the east and sets in the west.<|endofturn|>\n<|assistant|>\n", 45),
|
|
|
|
| 417 |
("Proverb Command", "<|user|>\nTranslate to Hindi: 'Consistency and discipline are the keys to long term success.'<|endofturn|>\n<|assistant|>\n", 50),
|
| 418 |
("Hinglish Command","<|user|>\nTranslate to Hinglish: 'I am waiting outside your office, please call me when you are free.'<|endofturn|>\n<|assistant|>\n", 50)
|
| 419 |
]
|
| 420 |
+
elif _vnum == "2":
|
| 421 |
test_suite = [
|
| 422 |
("Identity", "<|user|>\nWho created you and what is your name?<|endofturn|>\n<|assistant|>\n", 40),
|
| 423 |
("Indian Greet", "<|user|>\nRam Ram bhai! Kaise ho?<|endofturn|>\n<|assistant|>\n", 35),
|
|
|
|
| 514 |
|
| 515 |
ckpt_filename = "viutranslate_final.pt" if str(args.version).lower() == "viutranslate" else f"sft_{args.version}_final.pt"
|
| 516 |
save_path = os.path.join(output_dir, ckpt_filename)
|
| 517 |
+
# Best-checkpoint is kept in a SEPARATE file so later (worse) evals can never
|
| 518 |
+
# overwrite the best weights. Previously both shared save_path.
|
| 519 |
+
best_filename = "viutranslate_best.pt" if str(args.version).lower() == "viutranslate" else f"sft_{args.version}_best.pt"
|
| 520 |
+
best_save_path = os.path.join(output_dir, best_filename)
|
| 521 |
|
| 522 |
if args.init_ckpt:
|
| 523 |
init_ckpt = args.init_ckpt
|
|
|
|
| 533 |
tokenizer = None
|
| 534 |
tok_path = os.path.join(root_dir, "tokenizer", "tokenizer.json")
|
| 535 |
global PAD_TOKEN_ID, EOT_ID
|
| 536 |
+
# EOT-as-pad: padding must never reuse <|user|> (turn-start semantics).
|
| 537 |
+
PAD_TOKEN_ID = 64002
|
| 538 |
EOT_ID = 64002
|
| 539 |
+
MODEL_VOCAB = 64003
|
| 540 |
if os.path.exists(tok_path):
|
| 541 |
try:
|
| 542 |
from tokenizers import Tokenizer
|
| 543 |
tokenizer = Tokenizer.from_file(tok_path)
|
| 544 |
v = tokenizer.get_vocab()
|
|
|
|
| 545 |
EOT_ID = v.get("<|endofturn|>", 64002)
|
| 546 |
+
# EOT-as-pad: padding must never reuse <|user|> (turn-start semantics).
|
| 547 |
+
PAD_TOKEN_ID = EOT_ID
|
| 548 |
+
MODEL_VOCAB = tokenizer.get_vocab_size()
|
| 549 |
+
print(f"โ
Tokenizer bound dynamically: PAD_TOKEN_ID={PAD_TOKEN_ID}, EOT_ID={EOT_ID}, VOCAB={MODEL_VOCAB}")
|
| 550 |
except Exception as e:
|
| 551 |
print(f"โ ๏ธ Could not load tokenizer for special tokens ({e}). Using defaults.")
|
| 552 |
|
|
|
|
| 566 |
print(f"โ ๏ธ Notice: Could not parse metadata.json domain_names ({e}). Falling back to version heuristic.")
|
| 567 |
|
| 568 |
if not loaded_dynamic_domains:
|
| 569 |
+
# Exact normalized match only. Substring matching ("2" in "v26") previously
|
| 570 |
+
# mis-assigned v21/v22-v26 to the wrong domain map, corrupting per-domain telemetry.
|
| 571 |
+
vnorm = str(args.version).lower().replace("sft_", "").replace("sft", "").strip()
|
| 572 |
+
vnum = vnorm[1:] if vnorm.startswith("v") and vnorm[1:].isdigit() else vnorm
|
| 573 |
+
if vnorm == "viutranslate" or vnum in ("2",):
|
| 574 |
DOMAIN_NAMES = DOMAIN_NAMES_V2
|
| 575 |
+
elif vnum in ("1",):
|
| 576 |
DOMAIN_NAMES = DOMAIN_NAMES_V1
|
| 577 |
+
elif vnum in ("22", "23", "24", "25", "26"):
|
| 578 |
DOMAIN_NAMES = DOMAIN_NAMES_V22
|
| 579 |
+
elif vnum in ("19", "20", "21"):
|
| 580 |
DOMAIN_NAMES = DOMAIN_NAMES_V19
|
| 581 |
else:
|
| 582 |
DOMAIN_NAMES = DOMAIN_NAMES_V2
|
|
|
|
| 618 |
return None
|
| 619 |
return cand1
|
| 620 |
|
| 621 |
+
# 1. Load Data (resolve + validate shard paths BEFORE touching np.load so a
|
| 622 |
+
# wrong --version/--data_dir fails with a clear message, not a cryptic error)
|
| 623 |
+
_shard_paths = {
|
| 624 |
+
name: find_shard_path(stage_data_dir, name, args.version)
|
| 625 |
+
for name in ["train_tokens", "train_labels", "train_offsets",
|
| 626 |
+
"val_tokens", "val_labels", "val_offsets"]
|
| 627 |
+
}
|
| 628 |
+
for _label, _p in _shard_paths.items():
|
| 629 |
+
if _p is None or not os.path.exists(_p):
|
| 630 |
+
try:
|
| 631 |
+
_have = sorted(os.listdir(stage_data_dir))
|
| 632 |
+
except OSError:
|
| 633 |
+
_have = []
|
| 634 |
+
raise FileNotFoundError(
|
| 635 |
+
f"Missing required shard '{_label}*' for version '{args.version}' in: {stage_data_dir} "
|
| 636 |
+
f"(found: {_have}). Check --version/--data_dir, or run the dataset builder first."
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
train_dataset = SFTDataset(
|
| 640 |
+
ids_path=_shard_paths["train_tokens"],
|
| 641 |
+
labels_path=_shard_paths["train_labels"],
|
| 642 |
+
offsets_path=_shard_paths["train_offsets"],
|
| 643 |
domains_path=find_shard_path(stage_data_dir, "train_domains", args.version)
|
| 644 |
)
|
| 645 |
val_dataset = SFTDataset(
|
| 646 |
+
ids_path=_shard_paths["val_tokens"],
|
| 647 |
+
labels_path=_shard_paths["val_labels"],
|
| 648 |
+
offsets_path=_shard_paths["val_offsets"],
|
| 649 |
domains_path=find_shard_path(stage_data_dir, "val_domains", args.version)
|
| 650 |
)
|
| 651 |
|
|
|
|
| 680 |
print("๐พ Activation Checkpointing: ENABLED (low-VRAM mode)")
|
| 681 |
|
| 682 |
model_args = ViuAIConfig(
|
| 683 |
+
vocab_size=MODEL_VOCAB,
|
| 684 |
context_length=2048,
|
| 685 |
z_loss_weight=0.0,
|
| 686 |
attn_dropout=0.05,
|
|
|
|
| 800 |
inps = inps.to(device, non_blocking=True)
|
| 801 |
lbls = lbls.to(device, non_blocking=True)
|
| 802 |
with autocast_ctx:
|
| 803 |
+
# Single forward pass: get hidden states (with pad masking)
|
| 804 |
B, T = inps.shape
|
| 805 |
x = raw_m.tok_emb(inps)
|
| 806 |
cos = raw_m.rope_cos.to(x.device)
|
| 807 |
sin = raw_m.rope_sin.to(x.device)
|
| 808 |
+
eval_mask = None
|
| 809 |
+
if (inps == PAD_TOKEN_ID).any():
|
| 810 |
+
causal = torch.tril(torch.ones(T, T, dtype=torch.bool, device=x.device))
|
| 811 |
+
key_mask = (inps != PAD_TOKEN_ID).unsqueeze(1).unsqueeze(2)
|
| 812 |
+
bool_mask = causal.unsqueeze(0).unsqueeze(0) & key_mask
|
| 813 |
+
eval_mask = torch.zeros(B, 1, T, T, dtype=x.dtype, device=x.device)
|
| 814 |
+
eval_mask = eval_mask.masked_fill(~bool_mask, float("-inf"))
|
| 815 |
for block in raw_m.blocks:
|
| 816 |
+
x = block(x, cos, sin, eval_mask)
|
| 817 |
x = raw_m.final_norm(x)
|
| 818 |
hidden = x[:, :-1, :].contiguous() # [B, T-1, D]
|
| 819 |
shift_labels = lbls[:, 1:].contiguous() # [B, T-1]
|
|
|
|
| 896 |
total_tokens_trained += active_tokens_count
|
| 897 |
|
| 898 |
with autocast_ctx:
|
| 899 |
+
_, loss = model(inputs, targets=labels, pad_id=PAD_TOKEN_ID, shift_labels=True)
|
| 900 |
loss_scaled = loss / grad_acc
|
| 901 |
|
| 902 |
loss_scaled.backward()
|
|
|
|
| 966 |
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
| 967 |
}
|
| 968 |
torch.save(save_payload, save_path)
|
| 969 |
+
print(f" ๐พ Saved checkpoint -> {save_path}")
|
| 970 |
+
if is_best:
|
| 971 |
+
torch.save(save_payload, best_save_path)
|
| 972 |
+
print(f" ๐ Saved BEST checkpoint -> {best_save_path}")
|
| 973 |
|
| 974 |
# End of Epoch Handling
|
| 975 |
if epoch_batches > 0 and (micro_idx + 1) % grad_acc != 0:
|
|
|
|
| 1013 |
}
|
| 1014 |
torch.save(final_payload, save_path)
|
| 1015 |
print(f"๐พ Final master checkpoint saved to: {save_path}")
|
| 1016 |
+
if os.path.exists(best_save_path):
|
| 1017 |
+
print(f"๐ Best checkpoint (lowest val loss) kept at: {best_save_path}")
|
| 1018 |
|
| 1019 |
# Push to Hugging Face Hub (Only Final Checkpoint)
|
| 1020 |
if args.push_to_hf:
|
| 1021 |
print("\n๐ Pushing Final Checkpoint to Hugging Face Model Hub (ViuAI/ViuAI-500M)...")
|
| 1022 |
+
token = args.hf_token or os.environ.get("HF_TOKEN")
|
| 1023 |
if token:
|
| 1024 |
try:
|
| 1025 |
api = HfApi(token=token)
|