Skip to content

Rebuild the e2e training benchmarks around per-op MaxSim VRAM (ColQwen2 + PyLate)#102

Merged
tonywu71 merged 7 commits into
mainfrom
improve-training-e2e-benchmarks
Jun 5, 2026
Merged

Rebuild the e2e training benchmarks around per-op MaxSim VRAM (ColQwen2 + PyLate)#102
tonywu71 merged 7 commits into
mainfrom
improve-training-e2e-benchmarks

Conversation

@tonywu71

@tonywu71 tonywu71 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Context

The previous e2e training benches (bench_colpali_training/realdata.py, bench_pylate_training/realdata/lateon.py) ran hand-rolled step loops on synthetic data or small eval subsets and reported average step time plus whole-step peak memory. On encoder-dominated training steps that mostly measures parity (~1×), which is not where the kernel's value is.

colpali PR #412 built a better harness for its own validation: real trainer steps on real train data, the MaxSim op instrumented per call (forward transient, bytes held for backward), every recorded shape replayed on an isolated graph so the op's backward is bracketed exactly, and OOM treated as a recorded sweep outcome instead of a crash. What that surfaces (the op's B²-growing memory term and the batch-size ceiling it sets) survives encoder dilution where the step-time speedup does not.

This PR replaces the five old benches with two ports of that harness, kept as symmetrical as possible:

  • bench_colpali_e2e.py — ColQwen2 + LoRA through colpali-engine's own ColModelTraining recipe, on vidore/colpali_train_set shards (released colpali-engine==0.3.16, toggled with patch_colpali_engine()).
  • bench_pylate_e2e.py — GTE-ModernColBERT-v1 + Contrastive through SentenceTransformerTrainer, on MS MARCO triplets (pylate==1.5.0, toggled with patch_pylate()), with a --grad-checkpoint regime and --score-mini-batch-size cells covering PyLate's own chunking mitigation.

Benchmarking current PyLate required a product fix that rides along: PyLate 1.5 renamed the scoring module and rerouted its losses through ColBERTScores, which broke patch_pylate(). The old benches pinned 1.3.3 because of this; the patch now detects the layout, so the >=1.3.3,<2 range in the extra is accurate again.

Results (1× H100 80 GB)

Full tables and readings live in docs/benchmarks.md; per-cell JSONs are reproducible via the SkyPilot jobs below.

ColQwen2 reproduces colpali PR #412's published numbers to the MiB. The MaxSim op costs vanilla 7.81 GiB of VRAM at B=128 vs 61 MiB with LIK (~130×, ×4 vs ×2 per batch doubling); step time is at parity since the 2B encoder hides the op. Vanilla OOMs at B=128 (a 1.81 GiB request failing while 25 GiB sit reserved-but-unallocated, fragmentation caused by the multi-GiB score grid) where LIK trains it at 7.98 samples/s:

batch size vanilla LIK
64 29.54 GiB 29.54 GiB
128 OOM 54.38 GiB

colpali_e2e_vram colpali_e2e_ceiling

PyLate has two regimes and two different stories. On the canonical recipe (no grad-checkpointing) the encoder activations fill the card at B=256 before the score grid matters: vanilla, LIK, and PyLate's score_mini_batch_size chunking all OOM identically, so LIK only trims the op transient (1.8 GiB → 56 MiB at B=128) without moving the ceiling. With --grad-checkpoint, the regime matching the ColQwen2 bench, the B² transient binds and the 149 M encoder is too small to hide the op:

batch size vanilla vanilla + score_mini_batch_size=64 LIK
512 54.10 GiB · 2.69 s/step 30.60 GiB · 2.70 s/step 29.72 GiB · 2.40 s/step
1024 OOM (one 56.25 GiB grid alloc) 62.18 GiB · 6.02 s/step 57.67 GiB · 4.81 s/step

pylate_e2e_vram pylate_e2e_ceiling

The harness pins vanilla's B=1024 death inside the score call: a single contiguous allocation of exactly the [1024, 1024, 48, 300] fp32 per-slot grid. LIK is also 1.07–1.12× faster per step than vanilla here, and 1.25× faster than PyLate's own chunking at matched B=1024. Chunking buys the ceiling with serialized recompute; the fused kernel buys it while speeding the step up.

Quick start

sky launch scripts/sky_colpali_e2e.yaml -c lik-colpali-e2e -y
sky launch scripts/sky_pylate_e2e.yaml  -c lik-pylate-e2e  -y

rsync -avP 'lik-colpali-e2e:sky_workdir/benchmarks/results/' ./benchmarks/results/
python benchmarks/summarize_colpali_e2e.py --results-dir benchmarks/results --plot benchmarks/results/colpali_e2e_vram.png
python benchmarks/summarize_pylate_e2e.py  --results-dir benchmarks/results --regime ckpt

Note

  • bench_colpali_e2e.py ships a small ContrastiveTrainer shim for two colpali-engine 0.3.16 bugs under transformers 5.x (_get_train_sampler signature; single-dataset loss prefixes). Both are fixed upstream in colpali PR #412 but unreleased; the shim goes away once colpali-engine > 0.3.16 ships.
  • bench_colpali_loss.py (loss-head isolation, no encoder) is kept: it backs the 2.5–4.3× explicit-negative numbers already in the docs.
  • Historical result tables in docs/benchmarks.md stay; only references to removed scripts were updated.

Changes

Click here to expand
  • benchmarks/bench_colpali_e2e.py — ColQwen2 harness: per-MaxSim-call VRAM recording at the loss head, isolated-op replay (vanilla replays the release's exact einsum → amax → sum, lik replays lik.autograd.maxsim), step timer, OOM-as-outcome
  • benchmarks/bench_pylate_e2e.py — PyLate sibling: wraps the loss's score_metric, replays PyLate 1.5's exact per-slot ColBERTScores math vs the fused per-slot path; --grad-checkpoint and --score-mini-batch-size dimensions
  • summarize_colpali_e2e.py / summarize_pylate_e2e.py — op-VRAM and batch-ceiling markdown tables plus log-log plots from the per-cell JSONs (--regime plain|ckpt for pylate)
  • scripts/sky_colpali_e2e.yaml / sky_pylate_e2e.yaml — fresh process per (batch, variant) cell so OOMs are isolated; crash markers keep the sweep going; the colpali job is also the new home for bench_colpali_loss.py --sweep
  • patch_pylate() 1.5 support — resolve the defining module per layout (scores.scores for <=1.4, scores.colbert for >=1.5), rewrite only Distillation's import-time capture on 1.5; layout-aware CUDA compat tests
  • Removedbench_colpali_training/realdata.py, bench_pylate_training/realdata/lateon.py, _bench_common.py, sky_colpali_benchmark.yaml, sky_pylate_benchmark.yaml; pylate blocks dropped from sky_run_all_benchmarks.yaml
  • Docs — new ColQwen2 and PyLate e2e sections in docs/benchmarks.md with the measured tables, README benchmark/memory updates, CHANGELOG entries, benchmarks/README operator guide

Next steps

  • Once colpali PR #412 merges and a colpali-engine release ships it, drop the trainer shim here and migrate the colpali job to COLPALI_SCORES_BACKEND (per that PR's plan, the LIK-side patches eventually retire in favor of the native integrations).
  • Follow-up PR: reorganize benchmarks/ per stack — kernels/ (synthetic micro benches), plaid/, colpali/, pylate/ — with each summarize_* next to its bench, and rename bench_lateon.pykernels/bench_longdoc.py (it sweeps long-document shapes, and its e2e namesake is gone). Kept out of this PR so the benchmark work isn't buried in rename noise.

tonywu71 added 4 commits June 5, 2026 10:52
- Add bench_colpali_e2e.py (adapted from colpali PR #412): real
  ColModelTraining steps on vidore/colpali_train_set with the loss head
  instrumented per MaxSim call, isolated-op replay, OOM-as-outcome
- Add summarize_colpali_e2e.py (op-VRAM + batch-ceiling tables, log-log
  plot) and sky_colpali_e2e.yaml (fresh process per (B, variant) cell)
- Shim colpali-engine 0.3.16's ContrastiveTrainer bugs under
  transformers 5.x (fixed upstream in PR #412, unreleased)
- Remove the five superseded e2e benches, _bench_common.py, and the
  colpali/pylate sky benchmark jobs; keep bench_colpali_loss.py
- Update docs/benchmarks.md and README with measured H100 results:
  MaxSim op 7.81 GiB -> 61 MiB at B=128 (~130x), vanilla OOMs at B=128
  where LIK trains it (2x batch ceiling), step time at parity
- Resolve the defining module per layout: pylate.scores.scores (<=1.4)
  vs pylate.scores.colbert (>=1.5, where ColBERTScores resolves
  colbert_scores from module globals at call time)
- Rewrite only Distillation's import-time capture on the new layout;
  1.5's contrastive losses need no per-module rewriting
- Make the CUDA compat tests layout-aware
- Drop the stale "1.5 broke the patch / pin 1.3.3" notes
- Add bench_pylate_e2e.py: real SentenceTransformerTrainer steps
  (GTE-ModernColBERT-v1 + Contrastive on MS MARCO triplets) with the
  loss's score_metric instrumented per call, isolated-op replay,
  OOM-as-outcome; --variant vanilla|lik toggles patch_pylate()
- Expose PyLate's own mitigation via --score-mini-batch-size so the
  sweep compares LIK against built-in query-axis chunking
- Add summarize_pylate_e2e.py (leads with the forward transient:
  PyLate's .max(dim) holds indices, not the grid) and
  sky_pylate_e2e.yaml (fresh process per cell, chunked-vanilla cells
  at the largest batches)
- Add --grad-checkpoint to bench_pylate_e2e.py: on the canonical recipe
  the encoder activations cap the batch at B=128 (every variant OOMs at
  B=256 alike), so this regime — matching the ColQwen2 bench — is where
  the B^2 score transient binds
- Sweep both regimes in sky_pylate_e2e.yaml; summarizer gains --regime
  and regime-aware plot labels
- Record measured H100 results in docs/benchmarks.md, README, CHANGELOG:
  vanilla OOMs at B=1024 on a single 56.25 GiB grid alloc where LIK
  trains it at 4.81 s/step; step peak 54.1 -> 29.7 GiB at B=512; LIK
  1.07-1.12x faster per step than vanilla and 1.25x faster than PyLate's
  score_mini_batch_size chunking at matched B=1024
@tonywu71 tonywu71 self-assigned this Jun 5, 2026
@tonywu71 tonywu71 requested a review from h-aurelien-lac June 5, 2026 12:16
- Add --ceiling-plot to summarize_colpali_e2e.py and
  summarize_pylate_e2e.py: grouped bars of whole-step peak alloc per
  variant, hatched where a variant OOMs (the pre-OOM peak) — the
  batch-sweep figure from colpali PR #412's description, which the
  markdown table alone doesn't convey at a glance
- Mention the new flag in both sky yaml reproduce hints
@h-aurelien-lac

Copy link
Copy Markdown
Collaborator

Went through this carefully and cross-checked the harnesses against colpali-engine 0.3.16 and pylate 1.5.0 source — really clean, the hook points and the fp32-grid replay all line up with what the real trainers do. Just a few small things, nothing blocking:

  • In bench_pylate_e2e._replay_op_isolated, the replay masks are built with the embeddings dtype, but the real skiplist masks are bool — so the "allocation-identical to the real skiplist masks" comment is slightly off. It's sub-MiB so it doesn't move any number, but maybe cast to bool or soften the comment.
  • The slope annotations in summarize_colpali_e2e._write_plot ("×4 per doubling", "×2 per doubling") are hardcoded regardless of the data — fine today, but if a future run drifts the figure would assert a scaling the points don't show. Could derive them from the actual ratios.
  • Tiny one: we record step_peak_reserved_mib but neither summarizer surfaces it, even though the fragmentation story ("25 GiB reserved-but-unallocated") is really a reserved-vs-alloc thing. Either show it or drop the field.

The ~150 lines shared between the two benches (recorder/replay/timer) I assume is intentional given the benchmarks/ reorg in your next-steps — just confirming that's the plan and not drift.

- Replay pylate masks as bool (the dtype extract_skiplist_mask
  produces), and pass bool masks through without the != 0 copy the
  real patched path never makes
- Derive the op-VRAM plot's per-doubling slope annotations from the
  data instead of hardcoding x4/x2, so a drifting run can't produce a
  figure asserting a scaling the points don't show
- Surface step_peak_reserved_mib in both batch-ceiling tables
  (alloc / reserved) — fragmentation is a reserved-vs-alloc story
@tonywu71

tonywu71 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

Went through this carefully and cross-checked the harnesses against colpali-engine 0.3.16 and pylate 1.5.0 source — really clean, the hook points and the fp32-grid replay all line up with what the real trainers do. Just a few small things, nothing blocking:

  • In bench_pylate_e2e._replay_op_isolated, the replay masks are built with the embeddings dtype, but the real skiplist masks are bool — so the "allocation-identical to the real skiplist masks" comment is slightly off. It's sub-MiB so it doesn't move any number, but maybe cast to bool or soften the comment.
  • The slope annotations in summarize_colpali_e2e._write_plot ("×4 per doubling", "×2 per doubling") are hardcoded regardless of the data — fine today, but if a future run drifts the figure would assert a scaling the points don't show. Could derive them from the actual ratios.
  • Tiny one: we record step_peak_reserved_mib but neither summarizer surfaces it, even though the fragmentation story ("25 GiB reserved-but-unallocated") is really a reserved-vs-alloc thing. Either show it or drop the field.

The ~150 lines shared between the two benches (recorder/replay/timer) I assume is intentional given the benchmarks/ reorg in your next-steps — just confirming that's the plan and not drift.

Thanks for the careful pass — all four are right. Addressed in 841335f:

  1. Replay masks: confirmed extract_skiplist_mask returns bool (torch.logical_and), so the replay now builds bool masks instead of softening the comment. This also fixed a second subtlety: the lik replay's != 0 conversion was allocating a bool copy that the real patched path never makes (_mask_as_bool is a pass-through on bool). New _as_bool helper mirrors the patch exactly.
  2. Slope annotations: now derived from the last two points; a comment keeps the expected ×4/×2 as documentation of intent. On the current results the figure reads ×4.0 / ×2.1, so the claim holds with measured numbers.
  3. step_peak_reserved_mib: went with "show it" — both ceiling tables now print alloc / reserved per cell (colpali B=128: vanilla OOM (53.95 / 77.73 GiB pre-OOM) vs LIK 54.38 / 78.37 GiB, which makes the fragmentation mechanism visible in the table itself).
  4. Duplication: intentional — the harnesses are deliberately single-file so each reads standalone, same as the PR 412 original. Extracting the shared recorder/replay/timer into a common module is queued for the per-stack benchmarks/ reorg listed in Next steps.

Caveat: 1–2 touch GPU-only paths and are verified by lint + regenerated plots from the existing JSONs; the bool-mask replay gets its first real exercise on the next sweep. Sub-MiB delta, so I didn't relaunch an H100 for it. Will do in follow-up PR.

@h-aurelien-lac h-aurelien-lac left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM !!

@tonywu71 tonywu71 merged commit 6fcd753 into main Jun 5, 2026
8 checks passed
@tonywu71 tonywu71 deleted the improve-training-e2e-benchmarks branch June 5, 2026 13:21
h-tonywu pushed a commit that referenced this pull request Jun 5, 2026
The 1.3.3 pin and its 'pylate 1.5+ broke patch_pylate' comment predate
#102, which made patch_pylate() support the 1.5 score-module layout.
1.5.0 is the pinned baseline in docs/benchmarks.md and what
sky_pylate_e2e.yaml already installs.
tonywu71 added a commit that referenced this pull request Jun 5, 2026
)

* chore(benchmarks): remove stale one-off benches and orphaned scripts

- bench_backward_0_5.py: one-off v0.5.0 backward investigation; no docs
  tables back it, not part of any SkyPilot job.
- bench_fastplaid.py: isolated rerank microbench superseded by
  bench_fastplaid_e2e.py (same shapes, real on-disk index).
- bench_training.py: synthetic full-train-step sweep superseded by the
  e2e harnesses (bench_colpali_e2e / bench_pylate_e2e) and
  bench_backward_method.
- scripts/_bench_autotune_persistence.py + sky_bench_autotune_persistence.yaml:
  orphaned PR #64 reproducer; the feature is covered by tests since.

* refactor(benchmarks): group benches per comparison stack

benchmarks/ is now kernels/ (synthetic-tensor microbenches, incl. the
platform-specific bench_mps.py), plaid/, colpali/, and pylate/ — each e2e
harness sits next to its summarizer, and each SkyPilot job has an obvious
scope.

- bench_lateon.py -> kernels/bench_longdoc.py: its value is the
  long-document regime (Ld up to 16 384), not the LateOn brand. --only
  tags and the lateon_*.json output name are unchanged so existing
  results stay valid; the RUN_ONLY tag is now 'longdoc'.
- Pure moves otherwise (git mv): no behavior change, no JSON schema
  change. Sky yamls, run_all_benchmarks.sh, docs, and the benches' own
  usage docstrings point at the new paths.
- benchmarks/README.md restructured around the four folders and now
  documents that the e2e harnesses' shared machinery is deliberate
  duplication (standalone-by-design, per the #102 review).

* docs(benchmarks): qualify bench mentions with their new stack folder

First mention of each bench in docs/benchmarks.md now carries its
benchmarks/<stack>/ path, and the long-documents section names its
source (kernels/bench_longdoc.py, formerly bench_lateon.py) — it was
the only table section that never said which bench produced it.

* fix(scripts): bump sky_run_all's pylate pin to the 1.5.0 baseline

The 1.3.3 pin and its 'pylate 1.5+ broke patch_pylate' comment predate
#102, which made patch_pylate() support the 1.5 score-module layout.
1.5.0 is the pinned baseline in docs/benchmarks.md and what
sky_pylate_e2e.yaml already installs.

* docs: update bench glob references that slipped the path sweep

CONTRIBUTING.md's autotune section and the bug-report template still
pointed at the pre-reorg flat benchmarks/bench_*.py glob; the original
sweep didn't cover .github/ or CONTRIBUTING.md.

---------

Co-authored-by: Tony Wu <28306721+tonywu71@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants