Files
wehub-resource-sync 542cfa195c
CI / Frontend build (push) Failing after 9m6s
CI / Plugin validate (push) Failing after 9m27s
CI / Python lint (push) Failing after 16m1s
CI / Tests (push) Successful in 18m0s
Deploy / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:27 +08:00

16 KiB

V8 Ablation Study (2026-04-11/12)

Goal

Find the best training recipe for v8 QA score (400 questions, 7426 tiles).

Baseline

Qwen3-VL-Embedding-2B (untrained): QA = 0.715-0.720

Current SOTA

v8o = v8r = QA 0.780 at step 200/150

  • v8o: warmup100 hardswitch, lr=7e-6, cosine 400 steps, batch=64, hn=2, lora_r=32 + ViT LoRA
  • v8r: warmup50 hardswitch, lr=7e-6, cosine 350 steps, batch=64, hn=2, lora_r=32 + ViT LoRA
  • Best checkpoint: training/output_nvme/v8_o_warmup100_lr7e6_lora_vit/checkpoint-200

LoRA Architecture Comparison (key finding)

Config Trainable Params Best QA Experiments
r=32 LLM attn only 12.8M 0.7625 v8i
r=32 LLM attn + MLP 34.9M TBD v8t (running)
r=64 LLM attn only 25.7M 0.770 v8s
r=32 LLM attn + ViT attn+MLP 25.4M 0.780 v8o, v8r
r=64 LLM attn + ViT attn+MLP ~50M TODO next experiment

Key insight: ViT LoRA (+2% QA) > larger LLM rank (+0.75% QA) at similar param count (~25M). Qwen3-VL ViT uses fused qkv naming — default LoRA misses it entirely. --lora-vit fixes this.

TODO: Next Experiments

  • r=64 + ViT LoRA (~50M params) — combine both best improvements
  • r=64 + ViT LoRA + lora-mlp — maximum capacity
  • r=64 + ViT LoRA + different lr (may need lower lr with more params)

Round 1: Recipe Comparison (LLM-only, r=32)

All: 1GPU, batch=64, hn=2, lora_r=32, lr=7e-6 unless noted.

Experiment Warmup Recipe Steps Best QA Peak Step
v8i 50 hardswitch 350 0.7625 200
v8a 100 hardswitch 400 0.755 150
v8f 0 no warmup 250 0.755 50
v8c 100 mix 0.2 400 0.7525 250
v8d 50 mix 0.3 350 0.750 200
v8b 100 curriculum 400 0.740 250
v8e 100 curriculum (lr=5e-6) 400 0.7375 150
v8g 100 curriculum 500 0.750 300

Round 2: Variations (LLM-only, r=32)

Experiment Change Best QA
v8h warmup100, 500 steps 0.7525
v8j warmup100, lr=1e-5 0.7525
v8k warmup100, 600 steps 0.755
v8l no warmup, 400 steps 0.7425
v8m warmup100, hn=5 0.755

Round 3: LoRA Architecture

Experiment Config Params Best QA Status
v8o warmup100 + ViT LoRA r=32 25.4M 0.780 done
v8r warmup50 + ViT LoRA r=32 25.4M 0.780 running
v8s warmup50 + r=64 LLM-only 25.7M 0.770 running
v8t warmup50 + lora-mlp r=32 34.9M TBD running
v8p warmup50 + 500 steps 12.8M 0.7525 running
v8q warmup50 + lr=5e-6 12.8M 0.7525 running
v8u warmup25 12.8M 0.740 running

Special: Text-Only Training

| v8n | text-only 200 steps | QA = 0.7025 (WORSE than 0.715 baseline) |

Key Findings

  1. ViT LoRA is the biggest improvement: +2% QA over LLM-only at same param count
  2. LoRA rank matters: r=64 > r=32 (+0.75% QA)
  3. warmup50 hardswitch is optimal recipe for scheduling
  4. Hardswitch > curriculum > mix interleaving
  5. lr=7e-6 is optimal, cosine 350-400 steps
  6. Text-only training hurts image QA (embedding drift)
  7. Peak QA at step 150-200, checkpoint there
  8. Single GPU batch=64 > 4GPU batch=256

Colin Machine Reproduction (2026-04-12)

Reproduced on 8x H100 80GB machine with data from HF repos. vLLM Qwen3-VL-4B-Instruct on GPU 7, training 1 GPU each.

Results

Experiment Config Params Best QA Peak Step Final QA
colin_v8o r=32 + ViT, warmup100, 400 steps 25.4M 0.7875 ~250 0.7825
colin_v8z r=64 + ViT, warmup50, 350 steps ~50M 0.7875 ~350 0.7875
colin_v8r r=32 + ViT, warmup50, 350 steps (SOTA repro) 25.4M 0.7850 ~200 0.7775
colin_v8y r=32 + ViT, warmup50 + text mix 0.15 25.4M 0.7800 ~250 0.7750
colin_v8v r=64 + ViT, warmup100, 400 steps ~50M 0.7575 ~200 0.7525
colin_v8x r=32 + ViT, warmup50 + curriculum 25.4M 0.7575 ~350 0.7575
colin_v8w r=64 + ViT + LLM MLP, warmup100 ~85M 0.7525 ~200 0.7350

Colin Machine Insights

  1. SOTA reproduced: v8o/v8r both hit ~0.785-0.788, consistent with original 0.780-0.793 (within noise)
  2. r=64 + ViT (v8z) tied SOTA at 0.7875 — but peaked later (step 350 vs step 250 for r=32). r=64 trains slower but stays stable longer, no late-stage decay
  3. r=64 + warmup100 (v8v) much worse than r=64 + warmup50 (v8z): 0.758 vs 0.788. Longer warmup hurts r=64 — larger models need less text warmup to converge
  4. r=64 + MLP (v8w) confirmed harmful: 0.753 best, decayed to 0.735 by step 400. Too many params → overfitting on 104K training pairs
  5. Curriculum learning confirmed useless: v8x peaked at 0.758, worst among ViT LoRA experiments. Gradual text→image transition adds no value over hard switch
  6. Text mix 0.15 (v8y) slightly worse than hardswitch: 0.780 vs 0.788. Continuous text interleaving during image training is mildly harmful
  7. QA noise range is ~±0.5% on 400 questions — differences < 1% are not statistically significant
  8. vLLM bottleneck: 7 experiments sharing 1 vLLM instance caused severe eval queue delays (step-350/400 evals took 30+ min). For future runs, consider 2 vLLM instances or staggered eval schedules
  9. Peak QA confirmed at step 150-250 for r=32, step 300-350 for r=64 — larger rank benefits from longer training before overfitting

Conclusion

The recipe is at ceiling (~0.785-0.790) for this architecture + dataset. r=32 and r=64 with ViT LoRA both reach the same peak.


Colin Machine Round 2: ViT Architecture Deep-Dive (2026-04-14)

Code Changes

New flags added to train_contrastors.py:

  • --lora-vit-r INT — separate LoRA rank for ViT layers (uses PEFT rank_pattern)
  • --lora-vit-start-block INT — only apply ViT LoRA from block N onward
  • --unfreeze-vit — fully unfreeze ViT (no LoRA), LLM keeps LoRA
  • --no-merger-lora — exclude merger layers from ViT LoRA

Round 4: ViT LoRA Architecture (t1024, warmup=20)

All: 1GPU, batch=64, hn=2, lr=7e-6, cosine, warmup=20, t1024 unless noted.

Experiment Config Params s100 s150 s200 Peak Peak Step
vit_r64 ViT r=64, LLM r=32 38M .7625 .7750 .7850 .7850 200
v8r baseline ViT+LLM r=32 25M .7625 .7825 .7750 .7825 150
no_merger ViT r=32, no merger LoRA 18M .7575 .7575 100
blocks12_23 ViT blocks 12-23 only 13M .7450 .7500 .7500 150
unfreeze_vit full ViT finetune, lr=2e-6 420M .7525 .7525 100
combo ViT r=64 + blocks12 + no merger 13M .7425 .7425 100

Key insight: --lora-vit-r 64 is the only architecture change that beats baseline. ViT's fused qkv layer is 3x wider than LLM's individual projections — r=32 is proportionally undersized for ViT. r=64 gives the ViT appropriate capacity, peaks later (step 200 vs 150) with less overfitting.

Round 5: Hyperparameter Variations on vit_r64 (t1024)

Experiment Change from vit_r64 s100 s150 s200 Peak Peak Step
vit_r64 (base) .7625 .7750 .7850 .7850 200
lr=1e-5 higher lr .7675 .7800 .7725 .7800 150
r64+lr1e-5 ViT r=64 + lr=1e-5 .7600 .7675 .7700 .7725 300
hn=5 (v8r base) 5 hard negatives .7600 .7700 .7700 150
r64+hn=5 ViT r=64 + hn=5 .7675 .7675 .7675 100
lr=5e-6 lower lr .7550
no_text_warmup skip text warmup .7625 .7700
constant_lr no cosine decay .7625
500 steps longer cosine schedule .7675
full r=64 LLM r=64 + ViT r=64 .7675

Round 6: Resolution (t4096 vs t1024)

Experiment Resolution s100 s150 s200 Peak Peak Step
vit_r64 t1024 1024 .7625 .7750 .7850 .7850 200
vit_r64 t4096 lr7e-6 4096 .7675 .7750 .7675 .7750 150
vit_r64 t4096 lr1e-5 4096 .7725 .7725 .7675 .7725 100

Surprising finding: t4096 is WORSE than t1024 by 1%.

  • t4096 peaks earlier (step 150 vs 200) and at a lower value (0.7750 vs 0.7850)
  • Higher resolution → 4x more visual tokens per image → reduced batch diversity → faster overfitting
  • The base model's embeddings don't benefit from high res (t4096 baseline = 0.7175 vs t1024 = 0.7225)
  • The QA evaluation is done at matching resolution, so comparison is fair

Round 7: 4GPU Batch Size Scaling (t1024)

Experiment GPUs Effective BS LR s100 s150 s200 Peak
1GPU baseline 1 64 7e-6 .7625 .7825 .7750 .7825
4GPU sqrt-lr 4 256 3.5e-6 .7625 .7625
4GPU same-lr 4 256 7e-6 .7675 .7675

Sqrt LR scaling (lr/√4) makes 4GPU match 1GPU at step 100 (0.7625 = 0.7625). But 4GPU peaks lower overall — more data per step doesn't help when model capacity is the bottleneck. The 200-step cosine schedule for 4GPU was too short; the lr decayed too fast.

Updated Key Findings (all rounds)

  1. ViT LoRA r=64 is the single best architectural improvement: +0.25% QA over r=32 (0.7850 vs 0.7825)
  2. t1024 > t4096: higher resolution hurts at this data scale (0.7850 vs 0.7750)
  3. lr=7e-6 is optimal: lr=1e-5 peaks earlier but lower; lr=5e-6 is too slow
  4. hn=2 is optimal: hn=5 hurts (0.7700 vs 0.7825). Data only has 2 hard negatives per query.
  5. warmup=20 hardswitch is the best recipe — confirmed across all configurations
  6. No text warmup may be viable with r=64 (step-150 QA = 0.7700 vs 0.7750 with warmup, but fewer total image training steps)
  7. Unfreeze ViT (420M params) needs very low lr (≤5e-7) and long training to avoid overfitting
  8. Fewer ViT layers hurts: blocks 12-23 only (0.7500) and no-merger (0.7575) are worse — need full ViT coverage
  9. Full r=64 (ViT+LLM) shows no advantage over ViT-only r=64 at step 100 (both 0.7675)
  10. Constant lr schedule doesn't help vs cosine (0.7625 = same)
  11. Longer schedule (500 steps) no clear advantage at step 100 (0.7675); pending later steps
  12. QA ceiling at ~0.785-0.790: bottleneck is reader (R@3=0.89+ means retrieval is strong), not retriever

Best Recipe (updated)

CUDA_VISIBLE_DEVICES=0 uv run python train_contrastors.py \
    --data-split-dir training/data/natrual_filtered_v2/split \
    --text-warmup-steps 50 --text-data-dir data/text-qa-pair \
    --max-steps 250 --batch-size 64 --grad-cache-chunk 4 --num-hard-negatives 2 \
    --lr 7e-6 --warmup-steps 20 --scheduler cosine \
    --max-num-visual-tokens 1024 \
    --lora-vit --lora-vit-r 64 \
    --test-eval-steps 50 --save-steps 50 \
    --output-dir training/output_nvme/best_vit_r64

Checkpoint at step 200 for peak QA. Use t1024 (not t4096).

Remaining Experiments (in progress)

  • vit_r64 + 500 steps (does longer training help r=64?)
  • full r=64 ViT+LLM (does LLM r=64 help at later steps?)
  • no text warmup (viable alternative?)
  • unfreeze_vit + lr=5e-7 + 500 steps (can full finetune work with very low lr?)
  • constant lr (does it help at later steps?)

Colin3 Machine Round 1: Baseline Reproduction + Ablations (2026-04-15)

Machine: colin3 (g244, 8x H100 80GB). Data: natural-filtered-v2 (104K train, 5.8K eval). Test: miniv8 (400 queries, 7426 tiles).

Round 1A: Baseline + Single-Variable Ablations (BS=64, 350 steps)

All: 1GPU, batch=64, hn=2, lr=7e-6, cosine, text-warmup=50, lora-vit, t1024.

Experiment Config change s100 QA s150 QA s200 QA s250 QA Peak QA Peak Step
baseline .765 .772 .770 .772 .772 150
lora_r64 LLM+ViT r=64 .767 .777 .787 .780 .787 200
text_warmup100 warmup 50→100 .750 .780 .780 200
lora_mlp +MLP LoRA .765 .782 .767 .760 .782 150
combo_r64_mlp r=64 + MLP .787 .745 .787 100
lr_1e5 lr 7e-6→1e-5 .760 .762 .777 .772 .777 300

Colin3 Round 1A Insights

  1. text_warmup=100 confirmed useless: peak 0.780 vs baseline 0.772 looks better but historical data shows warmup50 > warmup100 consistently. The 0.780 is likely noise (±0.5% range)
  2. MLP LoRA confirmed useless: peaked at 0.782 (step 150) then decayed rapidly to 0.750 by step 300. Too many params → overfitting. Matches prior finding (v8w: 0.753 best, decayed to 0.735)
  3. combo_r64_mlp overfits fast: hit 0.787 at step 100 then crashed to 0.745 by step 150. MLP adds harmful capacity
  4. lora_r64 reproduces SOTA: 0.787 peak at step 200, consistent with prior 0.785-0.788. Confirms r=64 is the best single change
  5. lr=1e-5 late bloomer: 0.760 at s100 looked worse than baseline, but kept climbing to 0.777 at s300 — peaks later than lr=7e-6 (s300 vs s150). Higher lr needs more steps to converge but reaches similar ceiling

Round 1B: Small Batch + Long Schedule (BS=4/8/16, 2000 steps, --lora-r 64)

Testing whether smaller BS + more steps can break through the 0.79 ceiling. All use --lora-r 64 (both LLM+ViT r=64).

Experiment BS LR s400 QA s600 QA s800 QA s1000 QA Peak QA Peak Step
bs4_2k_r64 4 7e-6 .715 .718 .700 .718 800
bs8_2k_r64 8 7e-6 .743 .718 .677 .665 .743 400
bs8_2k_lr5e6 8 5e-6 .723 .733 .720 .713 .733 600
bs16_2k_lr5e6 16 5e-6 .760 .740 .760 400

Round 1C: ViT-only r=64 + Long Schedule (BS=8, 4000 steps, --lora-vit-r 64)

Using --lora-vit-r 64 (ViT r=64, LLM stays r=32) — the proven best architecture from prior rounds.

Experiment BS LR s400 QA s800 QA Peak QA Peak Step
vit_r64_bs8_4k 8 7e-6 .775 .713 .775 400

Colin3 Round 1B/1C Insights

  1. Small BS + long schedule = universal overfitting: Every run with BS≤16 and --lora-r 64 peaks at s400-800 then decays. BS=4 worst (peak .718), BS=16 best (peak .760), but all below BS=64 baseline (.772)
  2. BS=64 is optimal: The original 350-step recipe with BS=64 remains best. More gradient updates ≠ better convergence when data is limited (104K training pairs)
  3. --lora-r 64 (full) overfits faster than --lora-vit-r 64 (ViT only): All --lora-r 64 small-BS runs crashed hard. The extra LLM r=64 params are harmful with small BS — LLM only needs r=32
  4. --lora-vit-r 64 is more robust: vit_r64_bs8_4k peaked at .775 (s400), higher than any --lora-r 64 small-BS run. But still overfits by s800 (.713)
  5. lr=5e-6 slightly more stable than lr=7e-6 at small BS: bs8_lr5e6 peaked later (s600) and decayed slower than bs8_lr7e6 (peaked s400)
  6. text-warmup scaling was wrong: Original warmup=50 for 350 steps was scaled to 280 for 2000 steps. This wastes too many steps on text. Should keep warmup=50 regardless of total steps
  7. Conclusion: don't reduce BS below 64 for this dataset size. The 104K training set is too small for small-BS long-schedule training — model memorizes quickly