Visual Document Retrieval
Safetensors
sentence-transformers
colpali-engine
qwen3_5
vision-language
colbert
late-interaction
multi-vector
matryoshka
vidore
token-compression
Instructions to use tencent/EVIE-4.5B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use tencent/EVIE-4.5B with sentence-transformers:
from sentence_transformers import MultiVectorEncoder model = MultiVectorEncoder("tencent/EVIE-4.5B") queries = ["Which planet is known as the Red Planet?"] documents = [ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", ] query_embeddings = model.encode_query(queries) document_embeddings = model.encode_document(documents) similarities = model.similarity(query_embeddings, document_embeddings) print(similarities) - Notebooks
- Google Colab
- Kaggle
Download code/student/scripts/train.py from tencent/EVIE-4.5B: direct link, hf CLI and curl.
- Browser
- Download file 26 kB
-
https://huggingface.co/tencent/EVIE-4.5B/resolve/main/code/student/scripts/train.py
- Command line
-
hf download hf://tencent/EVIE-4.5B/code/student/scripts/train.py
-
curl -L -o train.py https://huggingface.co/tencent/EVIE-4.5B/resolve/main/code/student/scripts/train.py
26 kB
| #!/usr/bin/env python3 | |
| """Prefix-MRL student trainer with ARD. | |
| One maximum projection is initialized from Preview d128. A frozen EVIE-8B | |
| teacher transfers row/column MaxSim relations and hard-negative margins; the | |
| adapter-disabled Preview path anchors d128. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| import time | |
| import sys | |
| from pathlib import Path | |
| _SHARED = Path(__file__).resolve().parents[2] / "shared" | |
| if str(_SHARED) not in sys.path: | |
| sys.path.insert(0, str(_SHARED)) | |
| import torch | |
| from peft import LoraConfig | |
| from torch.distributed.elastic.multiprocessing.errors import record | |
| from transformers import TrainingArguments, set_seed | |
| from data_loader import ( | |
| ALLOWED_SOURCES, | |
| _default_dataset_cache_dir, | |
| build_hardneg_dataset, | |
| build_train_dataset, | |
| ) | |
| from paths import forbid_venv_path | |
| from colpali_engine.loss.late_interaction_losses import ColbertLoss, ColbertNegativeCELoss | |
| from colpali_engine.loss.ard import ARDLoss | |
| from colpali_engine.models import ColQwen3_5, ColQwen3_5Processor | |
| from transformers.models.qwen3_5 import Qwen3_5Config | |
| from colpali_engine.trainer.colmodel_training import ColModelTraining, ColModelTrainingConfig | |
| from colpali_engine.trainer.ard_trainer import ARDTrainer | |
| TARGET_MODULES = ( | |
| r"(.*(model)(?!.*visual).*(down_proj|gate_proj|up_proj|k_proj|q_proj|v_proj|o_proj|" | |
| r"in_proj_qkv|in_proj_z|in_proj_b|in_proj_a|out_proj).*$)" | |
| ) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--base-model", default="") | |
| parser.add_argument("--data-root", default="./data") | |
| parser.add_argument("--output-dir", required=True) | |
| parser.add_argument("--sources", nargs="*", default=[], choices=ALLOWED_SOURCES, | |
| help="Optional ablation filter on the source column; empty = full corpus.") | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--epochs", type=float, default=1.0) | |
| parser.add_argument("--max-steps", type=int, default=-1) | |
| parser.add_argument("--max-samples-per-source", type=int, default=0) | |
| parser.add_argument("--per-device-batch-size", type=int, default=2) | |
| parser.add_argument("--grad-accum", type=int, default=1) | |
| parser.add_argument("--learning-rate", type=float, default=1.5e-5, | |
| help="Warm-start default. Teacher from-scratch uses 4.57e-5.") | |
| parser.add_argument("--weight-decay", type=float, default=0.02) | |
| parser.add_argument("--warmup-ratio", type=float, default=0.08) | |
| parser.add_argument("--max-visual-tokens", type=int, default=1024) | |
| parser.add_argument("--col-dim", type=int, default=512, | |
| help="custom_text_proj output dim (unused when --head-dims is set).") | |
| parser.add_argument("--dataloader-workers", type=int, default=2) | |
| parser.add_argument( | |
| "--dataloader-prefetch-factor", | |
| type=int, | |
| default=4, | |
| help="Batches prefetched by each DataLoader worker; ignored when workers=0.", | |
| ) | |
| parser.add_argument("--save-steps", type=int, default=500) | |
| parser.add_argument("--logging-steps", type=int, default=10) | |
| parser.add_argument("--lora-r", type=int, default=32) | |
| parser.add_argument("--lora-alpha", type=int, default=128) | |
| parser.add_argument("--lora-dropout", type=float, default=0.197) | |
| parser.add_argument("--loss-temperature", type=float, default=0.02) | |
| parser.add_argument("--hardneg-in-batch-weight", type=float, default=0.5) | |
| parser.add_argument("--resume-from-checkpoint", default="", | |
| help="Checkpoint path, or 'latest' to resume the newest output checkpoint.") | |
| parser.add_argument("--attn", choices=("flash_attention_2", "sdpa", "eager"), default="flash_attention_2") | |
| parser.add_argument("--grad-checkpointing", choices=("on", "off"), default="off") | |
| parser.add_argument( | |
| "--bidirectional-attention", | |
| choices=("on", "off"), | |
| default="on", | |
| help="on = encoder-ize full-attention layers (ColEmbed V2).", | |
| ) | |
| parser.add_argument( | |
| "--hardneg-root", | |
| default="", | |
| help="Hardneg output with queries/. corpus/ is optional; without it images load from --data-root.", | |
| ) | |
| parser.add_argument("--num-hard-negs", type=int, default=2) | |
| parser.add_argument( | |
| "--use-hardnegatives", | |
| choices=("on", "off"), | |
| default="on", | |
| help="When --hardneg-root is set: on=ColbertNegativeCELoss; off=same rows, in-batch only.", | |
| ) | |
| parser.add_argument( | |
| "--report-to", | |
| default="none", | |
| help="Comma-separated metric trackers, e.g. wandb,tensorboard.", | |
| ) | |
| parser.add_argument("--logging-dir", default=None, help="TensorBoard event output directory.") | |
| parser.add_argument("--run-name", default=None, help="Run name for the tracker (e.g. wandb).") | |
| parser.add_argument( | |
| "--head-dims", | |
| default="", | |
| help="Comma-separated Prefix-MRL widths, e.g. '64,128,256,512,1024,2048'. " | |
| "Empty turns off prefixes and the teacher.", | |
| ) | |
| parser.add_argument( | |
| "--anchor-dim", | |
| type=int, | |
| default=128, | |
| help="Preview projection width copied into the Prefix-MRL prefix rows.", | |
| ) | |
| parser.add_argument( | |
| "--teacher-dir", | |
| default="", | |
| help="Directory of a frozen full-weight teacher (EVIE-8B). " | |
| "Empty together with --head-dims is retrieval-only.", | |
| ) | |
| parser.add_argument( | |
| "--teacher-md5", | |
| default="", | |
| help="Expected md5 of the teacher model.safetensors; verified before training starts.", | |
| ) | |
| parser.add_argument( | |
| "--kd-dims", | |
| default="64,128,256,512,1024,2048", | |
| help="Prefixes that receive relation distillation.", | |
| ) | |
| parser.add_argument("--calibration-dim", type=int, default=128) | |
| parser.add_argument("--teacher-temperature", type=float, default=0.13) | |
| parser.add_argument( | |
| "--student-temperatures", | |
| default="64:0.13,128:0.13,256:0.13,512:0.13,1024:0.13,2048:0.13", | |
| ) | |
| parser.add_argument("--relation-weight", type=float, default=1.0) | |
| parser.add_argument("--margin-weight", type=float, default=0.25) | |
| parser.add_argument("--anchor-weight", type=float, default=0.25) | |
| parser.add_argument("--column-weight", type=float, default=1.0) | |
| parser.add_argument("--confidence-floor", type=float, default=0.1) | |
| parser.add_argument("--teacher-wrong-factor", type=float, default=0.25) | |
| parser.add_argument("--head-weights", default="") | |
| parser.add_argument("--kd-head-weights", default="") | |
| parser.add_argument( | |
| "--kd-directions", | |
| choices=("both", "row", "column", "none"), | |
| default="both", | |
| ) | |
| parser.add_argument("--kd-include-hardnegs", choices=("on", "off"), default="on") | |
| parser.add_argument("--anchor-teacher", choices=("on", "off"), default="on") | |
| parser.add_argument("--task-consistent-batches", choices=("on", "off"), default="on") | |
| parser.add_argument("--gradient-target-ratio", type=float, default=0.5) | |
| parser.add_argument("--gradient-calibration-steps", type=int, default=100) | |
| parser.add_argument("--gradient-calibration-interval", type=int, default=10) | |
| parser.add_argument("--gradient-scale-min", type=float, default=0.05) | |
| parser.add_argument("--gradient-scale-max", type=float, default=20.0) | |
| parser.add_argument("--gradient-scale-ema", type=float, default=0.9) | |
| parser.add_argument("--gradient-diagnostics", choices=("on", "off"), default="on") | |
| parser.add_argument("--gradient-diagnostic-steps", type=int, default=100) | |
| parser.add_argument("--gradient-diagnostic-interval", type=int, default=10) | |
| parser.add_argument("--head-warmup-steps", type=int, default=100) | |
| return parser.parse_args() | |
| def resolve_pretrained(spec: str) -> str: | |
| path = Path(spec) | |
| return str(path.resolve()) if path.exists() else spec | |
| def parse_head_dims(spec: str) -> tuple[int, ...]: | |
| if not spec.strip(): | |
| return () | |
| dims = tuple(sorted({int(x) for x in spec.replace(" ", "").split(",") if x})) | |
| if not dims or dims[0] <= 0: | |
| raise ValueError(f"--head-dims must be positive integers, got {spec!r}") | |
| return dims | |
| def parse_dim_map(spec: str, allowed: tuple[int, ...], name: str) -> dict[int, float]: | |
| if not spec.strip(): | |
| return {} | |
| values: dict[int, float] = {} | |
| for item in spec.replace(" ", "").split(","): | |
| if not item: | |
| continue | |
| try: | |
| raw_dim, raw_value = item.split(":", 1) | |
| dim, value = int(raw_dim), float(raw_value) | |
| except ValueError as exc: | |
| raise ValueError(f"{name} expects d:value entries, got {item!r}") from exc | |
| if dim not in allowed: | |
| raise ValueError(f"{name} dim {dim} is not in {allowed}") | |
| values[dim] = value | |
| return values | |
| def file_md5(path: Path) -> str: | |
| digest = hashlib.md5() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(16 * 1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def load_teacher(teacher_dir: Path, expected_md5: str, attn: str, bidirectional: str): | |
| """Load the frozen teacher at full bf16 weight, eval + no_grad. | |
| Full weights only (merged soup, not base + adapter). Optional md5 check | |
| rejects a wrong teacher before training starts. | |
| Returns ``(teacher, verified_md5)``. The digest is reused for | |
| ``run_config.json`` so every rank does not hash the same file twice. | |
| """ | |
| weights = teacher_dir / "model.safetensors" | |
| shards = sorted(teacher_dir.glob("model-*-of-*.safetensors")) | |
| if not weights.is_file() and not shards: | |
| raise FileNotFoundError(f"teacher has no model*.safetensors: {teacher_dir}") | |
| verified_md5 = None | |
| if expected_md5: | |
| if not weights.is_file(): | |
| raise ValueError( | |
| f"--teacher-md5 given but teacher is sharded ({len(shards)} shards); " | |
| "cannot verify a single-file md5" | |
| ) | |
| verified_md5 = file_md5(weights) | |
| if verified_md5 != expected_md5: | |
| raise ValueError( | |
| f"teacher md5 mismatch: expected {expected_md5}, got {verified_md5} ({weights})" | |
| ) | |
| print(f"[teacher] md5 verified: {verified_md5}") | |
| config = Qwen3_5Config.from_pretrained(teacher_dir) | |
| if getattr(config, "head_dims", None): | |
| raise ValueError("teacher must be single-head; found head_dims in its config") | |
| teacher = ColQwen3_5.from_pretrained( | |
| teacher_dir, | |
| config=config, | |
| torch_dtype=torch.bfloat16, | |
| attn_implementation=attn, | |
| ) | |
| try: | |
| teacher.rope_deltas = None | |
| except AttributeError: | |
| pass | |
| if bidirectional == "on": | |
| teacher.enable_bidirectional_attention() | |
| teacher.eval() | |
| for p in teacher.parameters(): | |
| p.requires_grad_(False) | |
| print( | |
| f"[teacher] {teacher_dir.name}: dim={teacher.dim} " | |
| f"bidir={bidirectional} params={sum(p.numel() for p in teacher.parameters()):,}" | |
| ) | |
| return teacher, verified_md5 | |
| def assert_shared_batch(student, teacher, processor) -> None: | |
| checks = { | |
| "patch_size": (student.patch_size, teacher.patch_size, processor.image_processor.patch_size), | |
| "spatial_merge_size": ( | |
| student.spatial_merge_size, | |
| teacher.spatial_merge_size, | |
| processor.image_processor.merge_size, | |
| ), | |
| } | |
| for name, (s, t, p) in checks.items(): | |
| if not (int(s) == int(t) == int(p)): | |
| raise ValueError( | |
| f"{name} disagrees: student={s} teacher={t} processor={p}" | |
| ) | |
| for name in ("image_token_id", "video_token_id", "vision_start_token_id", "vocab_size"): | |
| s = getattr(student.config, name, None) | |
| t = getattr(teacher.config, name, None) | |
| if s != t: | |
| raise ValueError(f"{name} disagrees: student={s} teacher={t}") | |
| print( | |
| f"[teacher] shared-batch OK (patch={student.patch_size} " | |
| f"merge={student.spatial_merge_size} image_token_id={student.config.image_token_id})" | |
| ) | |
| def prepare_output(path: Path, resume_requested: bool) -> None: | |
| path = forbid_venv_path(path, "output-dir") | |
| prepared_by_launcher = os.environ.get("EVIE_OUTPUT_PREPARED") == "1" | |
| if path.exists() and any(path.iterdir()) and not resume_requested and not prepared_by_launcher: | |
| raise FileExistsError(f"Refusing to overwrite a non-empty output directory: {path}") | |
| path.mkdir(parents=True, exist_ok=True) | |
| def main() -> None: | |
| args = parse_args() | |
| head_dims = parse_head_dims(args.head_dims) | |
| kd_dims = parse_head_dims(args.kd_dims) | |
| if head_dims: | |
| if args.anchor_dim not in head_dims: | |
| raise ValueError(f"--anchor-dim {args.anchor_dim} must be in --head-dims {head_dims}") | |
| if not kd_dims or not set(kd_dims).issubset(head_dims): | |
| raise ValueError(f"--kd-dims {kd_dims} must be a non-empty subset of {head_dims}") | |
| if args.calibration_dim not in kd_dims: | |
| raise ValueError( | |
| f"--calibration-dim {args.calibration_dim} must be in --kd-dims {kd_dims}" | |
| ) | |
| student_temperatures = parse_dim_map( | |
| args.student_temperatures, kd_dims, "--student-temperatures" | |
| ) | |
| head_weights = parse_dim_map(args.head_weights, head_dims, "--head-weights") | |
| kd_head_weights = parse_dim_map(args.kd_head_weights, kd_dims, "--kd-head-weights") | |
| else: | |
| student_temperatures = {} | |
| head_weights = {} | |
| kd_head_weights = {} | |
| teacher_dir = Path(args.teacher_dir).resolve() if args.teacher_dir else None | |
| if teacher_dir is not None: | |
| if not head_dims: | |
| raise ValueError("--teacher-dir requires --head-dims") | |
| if not teacher_dir.is_dir(): | |
| raise FileNotFoundError(f"teacher dir is missing: {teacher_dir}") | |
| if args.margin_weight > 0 and args.kd_include_hardnegs != "on": | |
| raise ValueError("--margin-weight > 0 requires --kd-include-hardnegs on") | |
| output_dir = Path(args.output_dir).resolve() | |
| base_model = resolve_pretrained(args.base_model) | |
| data_root = Path(args.data_root).resolve() | |
| if not data_root.is_dir(): | |
| raise FileNotFoundError(f"Data root is missing: {data_root}") | |
| if args.hardneg_root: | |
| hn = Path(args.hardneg_root).resolve() | |
| subdir = os.environ.get("HARDNEG_SUBDIR", "allpos") | |
| if not (hn / subdir).is_dir(): | |
| raise FileNotFoundError(f"hardneg-root needs {subdir}/: {hn}") | |
| prepare_output(output_dir, resume_requested=bool(args.resume_from_checkpoint)) | |
| os.environ.setdefault("HF_DATASETS_CACHE", str(_default_dataset_cache_dir())) | |
| set_seed(args.seed) | |
| print("== building train-only query/image pairs ==") | |
| use_hardneg = bool(args.hardneg_root) and args.use_hardnegatives == "on" | |
| if teacher_dir is not None and args.margin_weight > 0 and not use_hardneg: | |
| raise ValueError("--margin-weight > 0 requires --hardneg-root and --use-hardnegatives on") | |
| if args.hardneg_root: | |
| train_dataset = build_hardneg_dataset( | |
| hardneg_root=args.hardneg_root, | |
| data_root=data_root, | |
| num_negatives=args.num_hard_negs, | |
| max_samples=args.max_samples_per_source, | |
| use_negatives=use_hardneg, | |
| ) | |
| else: | |
| train_dataset = build_train_dataset( | |
| data_root=data_root, | |
| sources=args.sources, | |
| max_samples_per_source=args.max_samples_per_source, | |
| ) | |
| print("== loading processor and bf16 base model ==") | |
| processor = ColQwen3_5Processor.from_pretrained( | |
| base_model, | |
| max_num_visual_tokens=args.max_visual_tokens, | |
| ) | |
| # Load Preview in its original d128 shape first, then expand to Prefix-MRL. | |
| # Setting head_dims before from_pretrained would shape-mismatch and drop | |
| # the deployed 128-d projection. | |
| model_config = Qwen3_5Config.from_pretrained(base_model) | |
| if not head_dims: | |
| model_config.dim = args.col_dim | |
| model = ColQwen3_5.from_pretrained( | |
| base_model, | |
| config=model_config, | |
| torch_dtype=torch.bfloat16, | |
| attn_implementation=args.attn, | |
| ) | |
| if head_dims: | |
| model.enable_prefix_mrl(head_dims, anchor_dim=args.anchor_dim) | |
| print( | |
| f"[model] prefix MRL = Linear({model.hidden_size_for_heads}->{max(head_dims)}), " | |
| f"dims={list(head_dims)}, copied Preview rows [0:{args.anchor_dim})" | |
| ) | |
| else: | |
| print(f"[model] custom_text_proj = Linear(-> {args.col_dim}), fresh full-rank head") | |
| try: | |
| model.rope_deltas = None | |
| except AttributeError: | |
| pass | |
| if args.bidirectional_attention == "on": | |
| model.enable_bidirectional_attention() | |
| print("[model] bidirectional attention enabled on full-attention layers") | |
| use_gc = args.grad_checkpointing == "on" | |
| if use_gc: | |
| model.enable_input_require_grads() | |
| report_to = [item.strip() for item in args.report_to.split(",") if item.strip()] | |
| if not report_to or report_to == ["none"]: | |
| report_to = [] | |
| training_args = TrainingArguments( | |
| output_dir=str(output_dir), | |
| num_train_epochs=args.epochs, | |
| max_steps=args.max_steps, | |
| per_device_train_batch_size=args.per_device_batch_size, | |
| gradient_accumulation_steps=args.grad_accum, | |
| gradient_checkpointing=use_gc, | |
| gradient_checkpointing_kwargs={"use_reentrant": False} if use_gc else None, | |
| dataloader_num_workers=args.dataloader_workers, | |
| dataloader_pin_memory=True, | |
| dataloader_persistent_workers=args.dataloader_workers > 0, | |
| dataloader_prefetch_factor=args.dataloader_prefetch_factor if args.dataloader_workers > 0 else None, | |
| dataloader_drop_last=True, | |
| save_steps=args.save_steps, | |
| save_total_limit=2, | |
| logging_steps=args.logging_steps, | |
| learning_rate=args.learning_rate, | |
| lr_scheduler_type="cosine", | |
| warmup_ratio=args.warmup_ratio, | |
| weight_decay=args.weight_decay, | |
| bf16=True, | |
| seed=args.seed, | |
| data_seed=args.seed, | |
| ddp_find_unused_parameters=False, | |
| report_to=report_to, | |
| logging_dir=args.logging_dir, | |
| run_name=args.run_name, | |
| ) | |
| head_modules = model.head_module_names() | |
| lora = LoraConfig( | |
| r=args.lora_r, | |
| lora_alpha=args.lora_alpha, | |
| lora_dropout=args.lora_dropout, | |
| init_lora_weights="gaussian", | |
| bias="none", | |
| task_type="FEATURE_EXTRACTION", | |
| target_modules=TARGET_MODULES, | |
| modules_to_save=head_modules, | |
| ) | |
| print(f"[lora] modules_to_save={head_modules}") | |
| teacher = None | |
| teacher_md5 = None | |
| trainer_cls = None | |
| trainer_kwargs: dict = {} | |
| if head_dims: | |
| if teacher_dir is not None: | |
| print(f"== loading frozen teacher: {teacher_dir} ==") | |
| teacher, teacher_md5 = load_teacher( | |
| teacher_dir, args.teacher_md5, args.attn, args.bidirectional_attention | |
| ) | |
| assert_shared_batch(model, teacher, processor) | |
| else: | |
| print("== no teacher: Prefix-MRL retrieval-only ==") | |
| print( | |
| f"== Loss: ARD (prefixes={list(head_dims)}, kd_dims={list(kd_dims)}, " | |
| f"tau_T={args.teacher_temperature}, tau_S={student_temperatures}, " | |
| f"relation={args.relation_weight}, margin={args.margin_weight}, " | |
| f"anchor={args.anchor_weight}, dirs={args.kd_directions}, " | |
| f"global_hardnegs={args.kd_include_hardnegs}, " | |
| f"task_consistent={args.task_consistent_batches}) ==" | |
| ) | |
| loss_func = ARDLoss( | |
| head_dims=head_dims, | |
| kd_dims=kd_dims, | |
| temperature=args.loss_temperature, | |
| teacher_temperature=args.teacher_temperature, | |
| student_temperatures=student_temperatures, | |
| relation_weight=args.relation_weight if teacher is not None else 0.0, | |
| margin_weight=args.margin_weight if teacher is not None else 0.0, | |
| anchor_weight=args.anchor_weight if args.anchor_teacher == "on" else 0.0, | |
| column_weight=args.column_weight, | |
| in_batch_term_weight=args.hardneg_in_batch_weight, | |
| kd_directions=args.kd_directions if teacher is not None else "none", | |
| confidence_floor=args.confidence_floor, | |
| teacher_wrong_factor=args.teacher_wrong_factor, | |
| head_weights=head_weights, | |
| kd_head_weights=kd_head_weights, | |
| pos_aware_negative_filtering=True, | |
| ) | |
| trainer_cls = ARDTrainer | |
| trainer_kwargs = { | |
| "teacher_model": teacher, | |
| "head_dims": head_dims, | |
| "anchor_dim": args.anchor_dim, | |
| "use_anchor_teacher": args.anchor_teacher == "on", | |
| "kd_include_hardnegs": args.kd_include_hardnegs == "on", | |
| "task_consistent_batches": args.task_consistent_batches == "on", | |
| "gradient_target_ratio": args.gradient_target_ratio, | |
| "gradient_calibration_steps": args.gradient_calibration_steps, | |
| "gradient_calibration_interval": args.gradient_calibration_interval, | |
| "gradient_scale_min": args.gradient_scale_min, | |
| "gradient_scale_max": args.gradient_scale_max, | |
| "gradient_scale_ema": args.gradient_scale_ema, | |
| "calibration_dim": args.calibration_dim, | |
| "gradient_diagnostics": args.gradient_diagnostics == "on", | |
| "gradient_diagnostic_steps": args.gradient_diagnostic_steps, | |
| "gradient_diagnostic_interval": args.gradient_diagnostic_interval, | |
| "head_warmup_steps": args.head_warmup_steps, | |
| } | |
| elif use_hardneg: | |
| judged = bool(getattr(train_dataset, "judged_pos", False)) | |
| all_pos = bool(getattr(train_dataset, "all_pos", False)) | |
| mode = ", judged-pos" if judged else (", all-pos" if all_pos else "") | |
| print( | |
| f"== Loss: ColbertNegativeCELoss (hard_negs={args.num_hard_negs}{mode}) ==" | |
| ) | |
| loss_func = ColbertNegativeCELoss( | |
| temperature=args.loss_temperature, | |
| normalize_scores=True, | |
| use_smooth_max=False, | |
| pos_aware_negative_filtering=True, | |
| in_batch_term_weight=args.hardneg_in_batch_weight, | |
| ) | |
| else: | |
| print("== Loss: ColbertLoss (in-batch only) ==") | |
| loss_func = ColbertLoss( | |
| temperature=args.loss_temperature, | |
| normalize_scores=True, | |
| use_smooth_max=False, | |
| ) | |
| trainer = ColModelTraining( | |
| ColModelTrainingConfig( | |
| output_dir=str(output_dir), | |
| processor=processor, | |
| model=model, | |
| train_dataset=train_dataset, | |
| eval_dataset=None, | |
| run_eval=False, | |
| loss_func=loss_func, | |
| tr_args=training_args, | |
| peft_config=lora, | |
| trainer_cls=trainer_cls, | |
| trainer_kwargs=trainer_kwargs, | |
| ) | |
| ) | |
| print("== training ==") | |
| started = time.time() | |
| resume = args.resume_from_checkpoint or None | |
| if resume == "latest": | |
| checkpoints = sorted( | |
| output_dir.glob("checkpoint-*"), | |
| key=lambda p: int(p.name.rsplit("-", 1)[-1]), | |
| ) | |
| if not checkpoints: | |
| raise FileNotFoundError(f"no checkpoint-* found under {output_dir}") | |
| resume = str(checkpoints[-1]) | |
| training_args.resume_from_checkpoint = resume | |
| trainer.train() | |
| trainer.save() | |
| world_size = int(os.environ.get("WORLD_SIZE", 1)) | |
| try: | |
| n_samples = len(train_dataset) | |
| except TypeError: | |
| n_samples = None | |
| config = vars(args) | { | |
| "base_model": str(base_model), | |
| "data_root": str(data_root), | |
| "framework": "evie-ard-prefix-mrl", | |
| "hardneg_subdir": ( | |
| os.environ.get("HARDNEG_SUBDIR", "allpos") if args.hardneg_root else None | |
| ), | |
| "judged_pos": bool(getattr(train_dataset, "judged_pos", False)), | |
| "all_pos": bool(getattr(train_dataset, "all_pos", False)), | |
| "head_dims": list(head_dims) if head_dims else None, | |
| "mrl_prefix": bool(head_dims), | |
| "anchor_dim": args.anchor_dim if head_dims else None, | |
| "kd_dims": list(kd_dims) if head_dims else None, | |
| "calibration_dim": args.calibration_dim if head_dims else None, | |
| "student_temperatures_parsed": ( | |
| loss_func.student_temperatures if head_dims else None | |
| ), | |
| "col_dim": max(head_dims) if head_dims else args.col_dim, | |
| "teacher_dir": str(teacher_dir) if teacher_dir else None, | |
| "teacher_md5": teacher_md5, | |
| "teacher_col_dim": int(teacher.dim) if teacher is not None else None, | |
| "kd_active": teacher is not None, | |
| "world_size": world_size, | |
| "effective_batch_size": args.per_device_batch_size * args.grad_accum * world_size, | |
| "train_samples": n_samples, | |
| "train_runtime_seconds": round(time.time() - started, 1), | |
| "total_params": sum(p.numel() for p in trainer.model.parameters()), | |
| "trainable_params": sum( | |
| p.numel() for p in trainer.model.parameters() if p.requires_grad | |
| ), | |
| } | |
| if int(os.environ.get("RANK", "0")) == 0: | |
| (output_dir / "run_config.json").write_text( | |
| json.dumps(config, ensure_ascii=False, indent=2) + "\n", | |
| encoding="utf-8", | |
| ) | |
| if torch.distributed.is_available() and torch.distributed.is_initialized(): | |
| torch.distributed.barrier() | |
| print(f"== complete: {output_dir} ==") | |
| def run_with_error_recording() -> None: | |
| """Persist the original failing DDP rank's traceback for torchrun.""" | |
| try: | |
| main() | |
| finally: | |
| if torch.distributed.is_available() and torch.distributed.is_initialized(): | |
| torch.distributed.destroy_process_group() | |
| if __name__ == "__main__": | |
| run_with_error_recording() | |