5.9 KiB
Training Developer Notes
This file keeps internal training notes that are useful for maintainers but too noisy for the top-level reproduction README.
Core Files
| File | Description |
|---|---|
train_contrastors.py |
Primary training script for the reproduced embedding fine-tune. Uses InfoNCE, hard negatives, GradCache, and optional ViT LoRA. |
mine_hard_negatives.py |
Mines near-miss documents with the base model for hard-negative training. |
filter_hard_negatives_vqa.py |
Filters mined retrieval candidates with a VLM so false negatives are not used as hard negatives. |
run_filter_hard_negatives_chunks.py |
Runs VQA hard-negative filtering in fixed-size chunks, useful for large JSONL files. |
verify_embeddings.py |
Compares base vs fine-tuned embeddings with similarity margins and retrieval metrics. |
tests/test_grad_equivalence.py |
Single-GPU gradient correctness tests for GradCache math. |
tests/test_grad_multi_gpu.py |
Multi-GPU DDP/gather/loss-scaling gradient correctness tests. |
Legacy / secondary scripts:
train_colpali.py— HF Trainer-based path. Simpler, but not the reproduced training run.train_swift.py— ms-swift alternative.train.py,model.py,dataset.py,evaluate.py— older local training/eval code.
Data Formats
Basic JSONL format:
{"query": "What is the population of Tokyo?", "chunk_path": "/path/to/chunk.png"}
Hard-negative format:
{"query": "...", "chunk_path": "...", "neg_chunk_paths": ["/path/to/neg1.png", "/path/to/neg2.png"]}
Hard-negative-with-retrieval-candidates format, used before VQA filtering:
{"query": "...", "chunk_path": "...", "neg_chunk_paths": ["..."], "retrieve_top20": [{"rank": 1, "path": "...", "score": 0.61}]}
Notes:
chunk_pathis resolved relative to the JSONL file when paths are relative.- Training images are screenshot chunks; last chunks can be smaller than the common tile size.
neg_chunk_pathsshould contain mined hard negatives, not random negatives.
Training Pipeline
- Data loading pre-validates images at init so all DDP ranks process the same number of batches.
BiQwen3Processorhandles text tokenization and image preprocessing with visual-token resolution control.BiQwen3embeds the text query and image document into single L2-normalized vectors.- InfoNCE is computed over the similarity matrix.
- With hard negatives, docs are interleaved as
[pos, neg1, neg2, pos, neg1, neg2, ...]. - Multi-GPU training uses
gather_with_gradso document embeddings from other ranks contribute gradients. - GradCache keeps activation memory tied to
--grad-cache-chunkrather than the full effective batch.
VQA Filtering For Hard Negatives
filter_hard_negatives_vqa.py removes false negatives from mined retrieval candidates:
- Read
retrieve_top20. - Skip the positive
chunk_path. - Check up to the first
Knon-positive candidates (--candidate-k, default10). - Ask the VLM to answer the query from each candidate image.
- Judge that answer on the same image.
- If verdict is
CORRECT, treat the candidate as a false negative and skip it. - If verdict is
WRONGorCANNOT_ANSWER, keep it as a hard negative. - Stop after collecting
--num-hard-negativeshard negatives. - Skip the example if not enough hard negatives are found within the first
Kcandidates.
Example:
OPENAI_API_KEY=... python filter_hard_negatives_vqa.py \
--input /tmp/sample_100_hn.jsonl \
--output /tmp/sample_100_hn_v2.jsonl \
--reviews-output /tmp/sample_100_hn_v2.reviews.jsonl \
--summary-output /tmp/sample_100_hn_v2.summary.json \
--candidate-k 10 \
--num-hard-negatives 2 \
--concurrency 8
For large files:
OPENAI_API_KEY=... python run_filter_hard_negatives_chunks.py \
--input training/data/lite-query-v2-full-filtered-hn.jsonl \
--output-dir training/data/lite-query-v2-full-filtered-hn-v2-chunks \
--chunk-size 10000 \
--candidate-k 10 \
--num-hard-negatives 2 \
--concurrency 8 \
--skip-existing
Each chunk folder contains:
filtered_hn.jsonlcandidate_reviews.jsonlsummary.json
summary.json is updated incrementally and tracks missing-path ratios for positive paths,
reviewed candidate paths, and all checked paths combined.
Dataset Packaging
For regenerating or uploading a Hugging Face dataset:
# Prepare HF dataset folder: convert absolute paths to relative paths and hardlink images.
python prepare_hf_dataset.py \
--split-dir training/data/lite-query-v2-full-filtered-hn-v2-chunks/split \
--image-root /opt/dlami/nvme/kiwix_tiles \
--output-dir hf_dataset_export/screenshot-training
# Package images into tar shards.
python package_hf_image_shards.py \
--source-dir hf_dataset_export/screenshot-training \
--output-dir hf_dataset_export_sharded/screenshot-training
# Upload to Hugging Face.
python upload_hf_dataset.py \
--local-dir hf_dataset_export_sharded/screenshot-training
Upload requires a Hugging Face token with write permission.
Test Commands
# Single-GPU: GradCache math + RandContext + clip_loss + rope_deltas
CUDA_VISIBLE_DEVICES=0 uv run python tests/test_grad_equivalence.py
# Multi-GPU: DDP + gather + loss scaling + gradient sync
CUDA_VISIBLE_DEVICES=0,1 uv run torchrun --nproc_per_node=2 tests/test_grad_multi_gpu.py
Archived / Experimental Mode
train_contrastors.py still has a query-side-tune mode that trains only the
query tower while keeping the doc/image tower frozen, so datastore embeddings do
not change. This is not part of the reproduction path in README.md.
Important caveats:
- Query-side retrieval eval depends on an external search API that accepts pre-computed
embeddingqueries. --query-side-backward directwas the stable path in smoke tests.--query-side-backward gradcachewas experimental and may hang in multi-GPU real-data runs.