miachen0401/cadrille

Python

0

147 commits

updated Aug 6, 2026

See the code

README

Cadrille — SFT + RL for image → CadQuery

Two-stage training stack for image → CadQuery code generation, built on Qwen3-VL-2B with a swappable backbone mixin (Qwen2-VL / Qwen3-VL today; Qwen2.5-VL adapter ready).

  1. SFT — supervised fine-tune on a 5-source CAD corpus (~789k items after dedup + drop) to emit CadQuery from pre-rendered 4-view 268×268 PNGs. Optional text-conditioning path for text2cad-bench.
  2. RL post-train — CPPO / DPO with mesh-IoU reward on top of an SFT checkpoint (cadrille paper algorithm, hardened for our pipeline).

Online IoU + Failures eval runs every eval_steps against fixed subsets of BenchCAD val / DeepCAD test / Fusion360 test. Greedy + max@8 (best of 8 candidates at t=1.0). Logged to W&B alongside eval_loss. Checkpoints push to a public/private HF model repo by a non-blocking thread on every save_steps.

Where the recipe lives: the active SFT config is configs/sft/iid.yaml (full-data control). The §7 paper study uses 4 sibling configs (baseline.yaml, ood.yaml, ood_enhance.yaml, iid.yaml); see configs/sft/README.md. Its header comments (lines 1-38) document mix design + per-source step share. Latest run learnings (data cleaning numbers, trajectory vs prior versions, op_loss interpretation, rendering pitfalls) are in docs/learnings_2026-04-29.md.


Quick Start — From Zero on a Fresh A100/H100 VM

Required tokens (in .env):

  • HF_TOKEN — Hula0401 read+write (for cad-sft data + ckpt push)
  • BenchCAD_HF_TOKEN — only if pushing back to BenchCAD/* upstream
  • WANDB_API_KEY
  • DISCORD_WEBHOOK_URL (optional) — for live eval-result posting
# 1. Clone
git clone https://github.com/miachen0401/cadrille.git
cd cadrille

# 2. Credentials
cp .env.example .env && $EDITOR .env

# 3. Install everything (apt deps, uv venv, torch+cu124, pytorch3d, cadquery,
#    flash-attn, Open3D source build w/ headless rendering, all training data)
bash scripts/setup.sh --data            # ~30-50 min wall-clock; idempotent

# 4. Reload PATH
source ~/.local/bin/env 2>/dev/null || source ~/.bashrc

# 5. Pre-flight check (catches bad pkl / missing PNGs / stale filter caches
#    BEFORE the GPU boots up — has saved several launches)
set -a && source .env && set +a
uv run python -m scripts.preflight_check \
    --config configs/sft/iid.yaml

# 6. Train iid (full-data baseline; 50 k steps, ~25 h on A100)
nohup uv run python -u -m train.sft \
    --config configs/sft/iid.yaml \
    > logs/iid_$(date +%Y%m%d_%H%M%S).log 2>&1 &

# 7. (optional) Live Discord posting — fires on every eval landing
nohup bash scripts/watch_eval_post_discord.sh \
    "$(ls -t logs/v3_*.log | head -1)" \
    /ephemeral/checkpoints/sft-s50k-lr2e-4-b8a4-img-<timestamp> \
    > logs/watch.log 2>&1 &

For RL: bash scripts/setup.sh --full then uv run python -m train.rl.train --config configs/rl/a100.yaml. RL is BLOCKED on SFT IoU ≥ 0.8 on DeepCAD/Fusion360 (greedy) — current ckpt is at ~0.6 greedy / 0.68 max@8.


Repository layout

common/              # cross-cutting helpers
  model.py           # Cadrille mixin (FourierPointEncoder + custom forward)
                     # works on any *VLForConditionalGeneration backbone
  datasets.py        # CadRecode / Text2CAD / BenchCad / CadRecode20k loaders
  meshio.py          # render_img + MeshDataset
  metrics.py         # compute_metrics / compute_reward / cq_reward_workers

train/
  sft/
    train.py         # entry  →  python -m train.sft --config <yaml>
    online_eval.py   # IoU + Failures + max@8 callback
    hf_uploader.py   # background ckpt push (non-blocking)
  rl/
    train.py algorithms/{cppo,dpo}.py  eval.py  mine.py  …

eval/
  bench_sweep.py     # multi-dataset × multi-temp × max@N IoU sweep
  passk.py           # pass@k estimator
  pipeline.py runner.py render.py report.py

data_prep/           # one-time data materializers (HF → local pkl + PNGs)
  fetch_cad_sft.py             # all 5 v3 SFT sources
  fetch_benchcad.py            # eval split
  prerender_dataset.py         # mesh → 4-view _render.png
  render_benchcad_easy.py      # cadquery → 4-view PNG, parallel + shard-aware
  merge_benchcad_easy_renders.py + upload_shards_to_hula0401.py

scripts/
  setup.sh                     # one-click installer (--data / --full)
  preflight_check.py           # MUST run before launching SFT
  watch_eval_post_discord.sh   # tail log → post IoU + collage to Discord
  analysis/                    # one-off diagnostics
    op_distribution_plot.py      # per-source op stats + plots
    source_grid_render.py        # 100-render grid per training source
    eval_to_discord.py           # full per-eval Discord post (history table,
                                 # comparison table, max@8, run lineage, etc)
    parse_cq.py                  # regex-based op extractor

bench/               # training-throughput benchmarks
experiments/         # off-main investigations (cadevolve, repair_lora, …)

configs/sft/         # YAML configs (one per recipe; v3 is current)
configs/rl/

tests/test_refactor_safety.py   # 81 import + math tests, ~5 s
docs/                # session reports, diagnostics, learnings_<date>.md

Data sources

All training data lives under Hula0401/cad-sft/<name>/ on HF and mirrors to data/<name>/. Use data_prep/fetch_cad_sft.py to pull.

v3 training mix (789k items, 60% HQ / 40% bench-stack)

sourceitemsmodestep %what it addswatch out
benchcad11kimg2%BC val anchor — the smallest source but highest BC val leverage (v2 saw +0.16 BC at 50× weight)tiny op vocab, over-fits if cranked too high
cad_iso_106122kimg23%only source with non-trivial fillet (19% of items) — drives rare-op recall on real partsfamily-clustered → must shuffle before render
benchcad_simple77kimg15%base-plane/workplane diversity, fast clean codeslowest op-count (median 4) — collapses if too heavy
text2cad_bench (img)53kimg5.5%extra image variants of t2c codesoverlaps with text variant — pick one mode per sample
text2cad_bench (text)53ktext5.5%only text-conditioned pathdescriptions vary in quality
cad_recode_bench472kimg49%the workhorse — wide op vocab, primary long-tail signal1.65 epochs/item; rare ops still mode-collapse at eval

Per-source op stats (n=500, seed=42): median ops/code is 4-5 across all sources except cad_recode_20k (7, NOT in v3). See docs/op_distribution_2026-04-29/ for plots.

v3 cleaning: 80% drop on trivial families + code-hash dedup removed 37% of benchcad / 24% of iso / 29% of t2c — pure compute saved.

Available but not in v3

  • benchcad-easy — 109k items, 55 shards, render_img 100% on Hula0401/cad-sft/benchcad-easy/. Same simple_* family as benchcad, 10× larger. Likely v4 candidate (+0.03-0.05 BC val expected).
  • cad_recode_20k — 19k legacy. Has the highest ops/case (median 7) but small; superseded by the 472k cad_recode_bench variant.

Evaluation buckets (held-out)

bucketsourcesizen_per_eval
BenchCAD vallocal 90/10 split of BenchCAD/cad_bench1,97350
DeepCAD testHula0401/deepCAD_test8,04650
Fusion360 testHula0401/fusion360_test_mesh1,72550
recode20k train (probe)sampled from training corpus–50

The RL gate is greedy IoU ≥ 0.8 on DeepCAD/Fusion360. Greedy + max@8 (8 candidates at t=1.0) report every eval_steps per bucket.


Configs

configmix / weightstotal weightbs × accmax_stepsnotes
configs/sft/iid.yamlbench 15 / iso 158 / simple 111 / easy 116 / t2c-img 55 / t2c-text 55 / recode-bench 49010008 × 4 = 3250,000Full-data control (no holdout). 60% HQ / 40% bench-stack. §7 IID upper bound.
configs/sft/ood_enhance.yamlsame as iid + 10-family holdout filter1000same50,000Holdout + benchcad-easy supplement. §7 OOD-with-supplement line.
configs/sft/ood.yamlbench 21 / iso 222 / simple 157 / t2c-img 55 / t2c-text 55 / recode-bench 4901000same50,000Holdout, no benchcad-easy. §7 OOD-plain line.
configs/sft/baseline.yamlt2c-img 53 / t2c-text 53 / recode-bench 472578same50,000HQ-only floor (no benchcad data). §7 no-bench line.
configs/sft/curriculum_qwen3vl_2b.yaml3-phase curriculumvaries8 × 420,000Plateaued early; phase-3 (8:1:1) hurt DC.
configs/sft/qwen3vl_2b_recode_30k_clean.yamlrecode-only (single source)–8 × 430,000Smoke / single-source baseline.
configs/sft/smoke.yamlminimal–1 × 1100CI-style end-to-end check.
configs/rl/a100.yamlDeepCAD/Fusion360 hard-minedper paper–12k+RL post-train (BLOCKED on SFT ≥ 0.8 greedy).

All historical configs are kept for reference. See docs/learnings_2026-04-29.md for the rationale of each version's mix change.


Eval

# Multi-temp, multi-sample sweep on a checkpoint
uv run python -m eval.bench_sweep \
    --ckpt /ephemeral/checkpoints/<run>/checkpoint-final \
    --base-model Qwen/Qwen3-VL-2B-Instruct \
    --backbone qwen3_vl \
    --datasets benchcad,deepcad,fusion360 \
    --temps 0,0.4,0.75,1.0 \
    --n-samples 8 --limit 50 --seed 42 \
    --modality img --batch-size 8 --score-workers 16 \
    --out eval_outputs/sweep_<tag>

# Pass@k
uv run python -m eval.passk \
    --checkpoint <ckpt> \
    --val-dir data/deepcad_test_mesh \
    --n-samples 5 --k-values 1,5

# Per-source op-distribution diagnostic (no GPU)
uv run python -m scripts.analysis.op_distribution_plot

Online (during training) eval is automatic; results post to W&B and (if configured) Discord. Each eval emits per-bucket greedy IoU + exec rate + op_loss + rare_op_recall; every 2nd eval also runs max@8.


Common tools (cheatsheet)

Frequently-used scripts, grouped by job. All are runnable as-is — no flags required for the common path. See each script's docstring for full options.

Training control & monitoring

ToolWhat it doesUsage
scripts/preflight_check.pyValidate every source in a YAML mix (pkl loadable, PNGs resolve, code lengths, filter-cache freshness). Run before every SFT launch.uv run python -m scripts.preflight_check --config configs/sft/<run>.yaml
scripts/launch_chain_runs.shWait for one SFT PID to exit, then launch baseline → ood → iid back-to-back (v3-722 + v4-OOD retro evals run between v4 stop and baseline start).WAIT_PID=<pid> nohup bash scripts/launch_chain_runs.sh > logs/launch_chain.log 2>&1 &
scripts/stop_v4_at_24k.shWatchdog: wait for checkpoint-{N}/model.safetensors to write + stabilise, then SIGTERM the training PID. Tunable target step / PID / ckpt root via env vars.V4_PID=<pid> TARGET_STEP=24000 nohup bash scripts/stop_v4_at_24k.sh > logs/stop.log 2>&1 &
scripts/watch_eval_post_discord.shPoll predictions/step-N.jsonl files, fire eval_to_discord.py per new step, mark .posted to avoid duplicates.nohup bash scripts/watch_eval_post_discord.sh <train.log> <ckpt-out-dir> > logs/watch.log 2>&1 &
scripts/analysis/eval_report.pyPer-step v3 vs v4 unified table (greedy + max@8 + ops, IID/OOD aware ★ on OOD row). Posts to Discord with --post.uv run python -m scripts.analysis.eval_report --step <N> --post
scripts/analysis/eval_to_discord.pyTrajectory collage poster — renders mesh trajectories per bucket, IID/OOD tagged anchors.called by the watcher above; or directly via --step N
Monitor indexFull catalogue + conventions (red [OOD] / blue [IID] tags, holdout sync rules).scripts/analysis/README_MONITOR.md

Render

ToolWhat it doesUsage
data_prep/prerender_dataset.pyRender all *.stl under a dir to {stem}_render.png (4-view 268×268). Idempotent, parallel-safe. Run once per dataset before training; common.meshio.render_img then loads PNGs instead of rendering on the fly.uv run python -m data_prep.prerender_dataset --dir data/<name> [--workers 8]
data_prep/render_benchcad_easy.pycadquery .py → 4-view 268×268 PNG, parallel + shard-aware (resumes per shard). Use for raw HF parquets that ship code-only.uv run python -m data_prep.render_benchcad_easy --shards 15-54 [--workers 8]
data_prep/merge_benchcad_easy_renders.pyMerge per-shard rendered PNGs into one parquet ready for HF upload.uv run python -m data_prep.merge_benchcad_easy_renders
common/meshio.py::render_imgLibrary function: load {stem}_render.png if it exists, else render on the fly via Open3D headless. Used at training time.from common.meshio import render_img; img = render_img(stl_path)

Offline eval (paper figures)

ToolWhat it doesUsage
scripts/eval_v3_cad_bench_722.pyv3 ckpt → full BenchCAD/cad_bench_722 (722 samples × 106 families). Saves per-case .py + metadata.jsonl + per-family summary.json. §7 IID upper-bound.uv run python scripts/eval_v3_cad_bench_722.py --ckpt <v3-ckpt> --out eval_outputs/v3_cad_bench_722
scripts/eval_v4_ood_retro.pyIterate saved checkpoints in a run dir, run stratified 50-OOD (10 fams × 5) on each. Backfills clean OOD trajectory for runs that pre-date the IID/OOD split refactor.uv run python scripts/eval_v4_ood_retro.py --ckpts <ckpt-root> --out eval_outputs/v4_ood_retro
eval/bench_sweep.pyMulti-dataset × multi-temp × max@N IoU sweep — see Eval section above.uv run python -m eval.bench_sweep ...
eval/passk.pypass@k estimator.uv run python -m eval.passk ...
scripts/analysis/main_figure_v2.py§7 4-panel paper figure: OOD rare_recall climb / OOD ess_pass plateau / final-step gap / IID control.uv run python -m scripts.analysis.main_figure_v2
scripts/analysis/v4_failure_analysis.pyPer-OOD-family IoU + exec + ess bars — appendix figure.uv run python -m scripts.analysis.v4_failure_analysis

Data prep (one-time)

ToolWhat it doesUsage
data_prep/fetch_cad_sft.pyPull all 5 v3 SFT sources from Hula0401/cad-sft/.uv run python -m data_prep.fetch_cad_sft
data_prep/fetch_benchcad.pyPull eval split (90/10 of BenchCAD/cad_bench). Generates STLs + writes _skipped.jsonl for tessellation failures.uv run python -m data_prep.fetch_benchcad
data_prep/build_holdout_split.pyPartition train.pkl → train_v4_holdout.pkl by held-out family list. Required before launching ood*.yaml configs.uv run python -m data_prep.build_holdout_split --holdout-families <fam1> <fam2> ...
data_prep/build_benchcad_easy.pyBuild data/benchcad-easy/{train,*.py,*.png} from local rendered shards (default range 15–54).uv run python -m data_prep.build_benchcad_easy [--shard-range 15-54]
data_prep/repack_recode_to_bench.pyAST-rewrite cad-recode codes to BenchCAD shell style (one-time; outputs already on HF).uv run python -m data_prep.repack_recode_to_bench

Tests

uv run pytest tests/test_refactor_safety.py    # 81 fast tests, ~5 s

tests/test_iou.py / test_pipeline.py / test_cppo_step.py cover GPU+ckpt-bound paths and should run after substantial training-loop changes.


Acknowledgments

Built on cadrille (ICLR 2026) — base VL backbone + FourierPointEncoder + CPPO algorithm. Our additions:

  • 5-source SFT data pipeline with cleaning (dedup + family-drop)
  • Backbone-agnostic Cadrille mixin (Qwen2 / Qwen3 / Qwen2.5)
  • online_eval.py IoU + max@8 callback
  • Non-blocking HF ckpt uploader
  • Discord live-eval poster with comparison + history tables
  • BenchCAD/benchcad-easy renderer (cadquery → 4-view 268×268)
  • Multi-VM render orchestration (shard-aware, family-shuffle, looser tessellation tolerance)

License: see upstream cadrille.

miachen0401/cadrille

Python

0

147 commits

updated Aug 6, 2026

See the code

README

Cadrille — SFT + RL for image → CadQuery

Two-stage training stack for image → CadQuery code generation, built on Qwen3-VL-2B with a swappable backbone mixin (Qwen2-VL / Qwen3-VL today; Qwen2.5-VL adapter ready).

  1. SFT — supervised fine-tune on a 5-source CAD corpus (~789k items after dedup + drop) to emit CadQuery from pre-rendered 4-view 268×268 PNGs. Optional text-conditioning path for text2cad-bench.
  2. RL post-train — CPPO / DPO with mesh-IoU reward on top of an SFT checkpoint (cadrille paper algorithm, hardened for our pipeline).

Online IoU + Failures eval runs every eval_steps against fixed subsets of BenchCAD val / DeepCAD test / Fusion360 test. Greedy + max@8 (best of 8 candidates at t=1.0). Logged to W&B alongside eval_loss. Checkpoints push to a public/private HF model repo by a non-blocking thread on every save_steps.

Where the recipe lives: the active SFT config is configs/sft/iid.yaml (full-data control). The §7 paper study uses 4 sibling configs (baseline.yaml, ood.yaml, ood_enhance.yaml, iid.yaml); see configs/sft/README.md. Its header comments (lines 1-38) document mix design + per-source step share. Latest run learnings (data cleaning numbers, trajectory vs prior versions, op_loss interpretation, rendering pitfalls) are in docs/learnings_2026-04-29.md.


Quick Start — From Zero on a Fresh A100/H100 VM

Required tokens (in .env):

  • HF_TOKEN — Hula0401 read+write (for cad-sft data + ckpt push)
  • BenchCAD_HF_TOKEN — only if pushing back to BenchCAD/* upstream
  • WANDB_API_KEY
  • DISCORD_WEBHOOK_URL (optional) — for live eval-result posting
# 1. Clone
git clone https://github.com/miachen0401/cadrille.git
cd cadrille

# 2. Credentials
cp .env.example .env && $EDITOR .env

# 3. Install everything (apt deps, uv venv, torch+cu124, pytorch3d, cadquery,
#    flash-attn, Open3D source build w/ headless rendering, all training data)
bash scripts/setup.sh --data            # ~30-50 min wall-clock; idempotent

# 4. Reload PATH
source ~/.local/bin/env 2>/dev/null || source ~/.bashrc

# 5. Pre-flight check (catches bad pkl / missing PNGs / stale filter caches
#    BEFORE the GPU boots up — has saved several launches)
set -a && source .env && set +a
uv run python -m scripts.preflight_check \
    --config configs/sft/iid.yaml

# 6. Train iid (full-data baseline; 50 k steps, ~25 h on A100)
nohup uv run python -u -m train.sft \
    --config configs/sft/iid.yaml \
    > logs/iid_$(date +%Y%m%d_%H%M%S).log 2>&1 &

# 7. (optional) Live Discord posting — fires on every eval landing
nohup bash scripts/watch_eval_post_discord.sh \
    "$(ls -t logs/v3_*.log | head -1)" \
    /ephemeral/checkpoints/sft-s50k-lr2e-4-b8a4-img-<timestamp> \
    > logs/watch.log 2>&1 &

For RL: bash scripts/setup.sh --full then uv run python -m train.rl.train --config configs/rl/a100.yaml. RL is BLOCKED on SFT IoU ≥ 0.8 on DeepCAD/Fusion360 (greedy) — current ckpt is at ~0.6 greedy / 0.68 max@8.


Repository layout

common/              # cross-cutting helpers
  model.py           # Cadrille mixin (FourierPointEncoder + custom forward)
                     # works on any *VLForConditionalGeneration backbone
  datasets.py        # CadRecode / Text2CAD / BenchCad / CadRecode20k loaders
  meshio.py          # render_img + MeshDataset
  metrics.py         # compute_metrics / compute_reward / cq_reward_workers

train/
  sft/
    train.py         # entry  →  python -m train.sft --config <yaml>
    online_eval.py   # IoU + Failures + max@8 callback
    hf_uploader.py   # background ckpt push (non-blocking)
  rl/
    train.py algorithms/{cppo,dpo}.py  eval.py  mine.py  …

eval/
  bench_sweep.py     # multi-dataset × multi-temp × max@N IoU sweep
  passk.py           # pass@k estimator
  pipeline.py runner.py render.py report.py

data_prep/           # one-time data materializers (HF → local pkl + PNGs)
  fetch_cad_sft.py             # all 5 v3 SFT sources
  fetch_benchcad.py            # eval split
  prerender_dataset.py         # mesh → 4-view _render.png
  render_benchcad_easy.py      # cadquery → 4-view PNG, parallel + shard-aware
  merge_benchcad_easy_renders.py + upload_shards_to_hula0401.py

scripts/
  setup.sh                     # one-click installer (--data / --full)
  preflight_check.py           # MUST run before launching SFT
  watch_eval_post_discord.sh   # tail log → post IoU + collage to Discord
  analysis/                    # one-off diagnostics
    op_distribution_plot.py      # per-source op stats + plots
    source_grid_render.py        # 100-render grid per training source
    eval_to_discord.py           # full per-eval Discord post (history table,
                                 # comparison table, max@8, run lineage, etc)
    parse_cq.py                  # regex-based op extractor

bench/               # training-throughput benchmarks
experiments/         # off-main investigations (cadevolve, repair_lora, …)

configs/sft/         # YAML configs (one per recipe; v3 is current)
configs/rl/

tests/test_refactor_safety.py   # 81 import + math tests, ~5 s
docs/                # session reports, diagnostics, learnings_<date>.md

Data sources

All training data lives under Hula0401/cad-sft/<name>/ on HF and mirrors to data/<name>/. Use data_prep/fetch_cad_sft.py to pull.

v3 training mix (789k items, 60% HQ / 40% bench-stack)

sourceitemsmodestep %what it addswatch out
benchcad11kimg2%BC val anchor — the smallest source but highest BC val leverage (v2 saw +0.16 BC at 50× weight)tiny op vocab, over-fits if cranked too high
cad_iso_106122kimg23%only source with non-trivial fillet (19% of items) — drives rare-op recall on real partsfamily-clustered → must shuffle before render
benchcad_simple77kimg15%base-plane/workplane diversity, fast clean codeslowest op-count (median 4) — collapses if too heavy
text2cad_bench (img)53kimg5.5%extra image variants of t2c codesoverlaps with text variant — pick one mode per sample
text2cad_bench (text)53ktext5.5%only text-conditioned pathdescriptions vary in quality
cad_recode_bench472kimg49%the workhorse — wide op vocab, primary long-tail signal1.65 epochs/item; rare ops still mode-collapse at eval

Per-source op stats (n=500, seed=42): median ops/code is 4-5 across all sources except cad_recode_20k (7, NOT in v3). See docs/op_distribution_2026-04-29/ for plots.

v3 cleaning: 80% drop on trivial families + code-hash dedup removed 37% of benchcad / 24% of iso / 29% of t2c — pure compute saved.

Available but not in v3

  • benchcad-easy — 109k items, 55 shards, render_img 100% on Hula0401/cad-sft/benchcad-easy/. Same simple_* family as benchcad, 10× larger. Likely v4 candidate (+0.03-0.05 BC val expected).
  • cad_recode_20k — 19k legacy. Has the highest ops/case (median 7) but small; superseded by the 472k cad_recode_bench variant.

Evaluation buckets (held-out)

bucketsourcesizen_per_eval
BenchCAD vallocal 90/10 split of BenchCAD/cad_bench1,97350
DeepCAD testHula0401/deepCAD_test8,04650
Fusion360 testHula0401/fusion360_test_mesh1,72550
recode20k train (probe)sampled from training corpus–50

The RL gate is greedy IoU ≥ 0.8 on DeepCAD/Fusion360. Greedy + max@8 (8 candidates at t=1.0) report every eval_steps per bucket.


Configs

configmix / weightstotal weightbs × accmax_stepsnotes
configs/sft/iid.yamlbench 15 / iso 158 / simple 111 / easy 116 / t2c-img 55 / t2c-text 55 / recode-bench 49010008 × 4 = 3250,000Full-data control (no holdout). 60% HQ / 40% bench-stack. §7 IID upper bound.
configs/sft/ood_enhance.yamlsame as iid + 10-family holdout filter1000same50,000Holdout + benchcad-easy supplement. §7 OOD-with-supplement line.
configs/sft/ood.yamlbench 21 / iso 222 / simple 157 / t2c-img 55 / t2c-text 55 / recode-bench 4901000same50,000Holdout, no benchcad-easy. §7 OOD-plain line.
configs/sft/baseline.yamlt2c-img 53 / t2c-text 53 / recode-bench 472578same50,000HQ-only floor (no benchcad data). §7 no-bench line.
configs/sft/curriculum_qwen3vl_2b.yaml3-phase curriculumvaries8 × 420,000Plateaued early; phase-3 (8:1:1) hurt DC.
configs/sft/qwen3vl_2b_recode_30k_clean.yamlrecode-only (single source)–8 × 430,000Smoke / single-source baseline.
configs/sft/smoke.yamlminimal–1 × 1100CI-style end-to-end check.
configs/rl/a100.yamlDeepCAD/Fusion360 hard-minedper paper–12k+RL post-train (BLOCKED on SFT ≥ 0.8 greedy).

All historical configs are kept for reference. See docs/learnings_2026-04-29.md for the rationale of each version's mix change.


Eval

# Multi-temp, multi-sample sweep on a checkpoint
uv run python -m eval.bench_sweep \
    --ckpt /ephemeral/checkpoints/<run>/checkpoint-final \
    --base-model Qwen/Qwen3-VL-2B-Instruct \
    --backbone qwen3_vl \
    --datasets benchcad,deepcad,fusion360 \
    --temps 0,0.4,0.75,1.0 \
    --n-samples 8 --limit 50 --seed 42 \
    --modality img --batch-size 8 --score-workers 16 \
    --out eval_outputs/sweep_<tag>

# Pass@k
uv run python -m eval.passk \
    --checkpoint <ckpt> \
    --val-dir data/deepcad_test_mesh \
    --n-samples 5 --k-values 1,5

# Per-source op-distribution diagnostic (no GPU)
uv run python -m scripts.analysis.op_distribution_plot

Online (during training) eval is automatic; results post to W&B and (if configured) Discord. Each eval emits per-bucket greedy IoU + exec rate + op_loss + rare_op_recall; every 2nd eval also runs max@8.


Common tools (cheatsheet)

Frequently-used scripts, grouped by job. All are runnable as-is — no flags required for the common path. See each script's docstring for full options.

Training control & monitoring

ToolWhat it doesUsage
scripts/preflight_check.pyValidate every source in a YAML mix (pkl loadable, PNGs resolve, code lengths, filter-cache freshness). Run before every SFT launch.uv run python -m scripts.preflight_check --config configs/sft/<run>.yaml
scripts/launch_chain_runs.shWait for one SFT PID to exit, then launch baseline → ood → iid back-to-back (v3-722 + v4-OOD retro evals run between v4 stop and baseline start).WAIT_PID=<pid> nohup bash scripts/launch_chain_runs.sh > logs/launch_chain.log 2>&1 &
scripts/stop_v4_at_24k.shWatchdog: wait for checkpoint-{N}/model.safetensors to write + stabilise, then SIGTERM the training PID. Tunable target step / PID / ckpt root via env vars.V4_PID=<pid> TARGET_STEP=24000 nohup bash scripts/stop_v4_at_24k.sh > logs/stop.log 2>&1 &
scripts/watch_eval_post_discord.shPoll predictions/step-N.jsonl files, fire eval_to_discord.py per new step, mark .posted to avoid duplicates.nohup bash scripts/watch_eval_post_discord.sh <train.log> <ckpt-out-dir> > logs/watch.log 2>&1 &
scripts/analysis/eval_report.pyPer-step v3 vs v4 unified table (greedy + max@8 + ops, IID/OOD aware ★ on OOD row). Posts to Discord with --post.uv run python -m scripts.analysis.eval_report --step <N> --post
scripts/analysis/eval_to_discord.pyTrajectory collage poster — renders mesh trajectories per bucket, IID/OOD tagged anchors.called by the watcher above; or directly via --step N
Monitor indexFull catalogue + conventions (red [OOD] / blue [IID] tags, holdout sync rules).scripts/analysis/README_MONITOR.md

Render

ToolWhat it doesUsage
data_prep/prerender_dataset.pyRender all *.stl under a dir to {stem}_render.png (4-view 268×268). Idempotent, parallel-safe. Run once per dataset before training; common.meshio.render_img then loads PNGs instead of rendering on the fly.uv run python -m data_prep.prerender_dataset --dir data/<name> [--workers 8]
data_prep/render_benchcad_easy.pycadquery .py → 4-view 268×268 PNG, parallel + shard-aware (resumes per shard). Use for raw HF parquets that ship code-only.uv run python -m data_prep.render_benchcad_easy --shards 15-54 [--workers 8]
data_prep/merge_benchcad_easy_renders.pyMerge per-shard rendered PNGs into one parquet ready for HF upload.uv run python -m data_prep.merge_benchcad_easy_renders
common/meshio.py::render_imgLibrary function: load {stem}_render.png if it exists, else render on the fly via Open3D headless. Used at training time.from common.meshio import render_img; img = render_img(stl_path)

Offline eval (paper figures)

ToolWhat it doesUsage
scripts/eval_v3_cad_bench_722.pyv3 ckpt → full BenchCAD/cad_bench_722 (722 samples × 106 families). Saves per-case .py + metadata.jsonl + per-family summary.json. §7 IID upper-bound.uv run python scripts/eval_v3_cad_bench_722.py --ckpt <v3-ckpt> --out eval_outputs/v3_cad_bench_722
scripts/eval_v4_ood_retro.pyIterate saved checkpoints in a run dir, run stratified 50-OOD (10 fams × 5) on each. Backfills clean OOD trajectory for runs that pre-date the IID/OOD split refactor.uv run python scripts/eval_v4_ood_retro.py --ckpts <ckpt-root> --out eval_outputs/v4_ood_retro
eval/bench_sweep.pyMulti-dataset × multi-temp × max@N IoU sweep — see Eval section above.uv run python -m eval.bench_sweep ...
eval/passk.pypass@k estimator.uv run python -m eval.passk ...
scripts/analysis/main_figure_v2.py§7 4-panel paper figure: OOD rare_recall climb / OOD ess_pass plateau / final-step gap / IID control.uv run python -m scripts.analysis.main_figure_v2
scripts/analysis/v4_failure_analysis.pyPer-OOD-family IoU + exec + ess bars — appendix figure.uv run python -m scripts.analysis.v4_failure_analysis

Data prep (one-time)

ToolWhat it doesUsage
data_prep/fetch_cad_sft.pyPull all 5 v3 SFT sources from Hula0401/cad-sft/.uv run python -m data_prep.fetch_cad_sft
data_prep/fetch_benchcad.pyPull eval split (90/10 of BenchCAD/cad_bench). Generates STLs + writes _skipped.jsonl for tessellation failures.uv run python -m data_prep.fetch_benchcad
data_prep/build_holdout_split.pyPartition train.pkl → train_v4_holdout.pkl by held-out family list. Required before launching ood*.yaml configs.uv run python -m data_prep.build_holdout_split --holdout-families <fam1> <fam2> ...
data_prep/build_benchcad_easy.pyBuild data/benchcad-easy/{train,*.py,*.png} from local rendered shards (default range 15–54).uv run python -m data_prep.build_benchcad_easy [--shard-range 15-54]
data_prep/repack_recode_to_bench.pyAST-rewrite cad-recode codes to BenchCAD shell style (one-time; outputs already on HF).uv run python -m data_prep.repack_recode_to_bench

Tests

uv run pytest tests/test_refactor_safety.py    # 81 fast tests, ~5 s

tests/test_iou.py / test_pipeline.py / test_cppo_step.py cover GPU+ckpt-bound paths and should run after substantial training-loop changes.


Acknowledgments

Built on cadrille (ICLR 2026) — base VL backbone + FourierPointEncoder + CPPO algorithm. Our additions:

  • 5-source SFT data pipeline with cleaning (dedup + family-drop)
  • Backbone-agnostic Cadrille mixin (Qwen2 / Qwen3 / Qwen2.5)
  • online_eval.py IoU + max@8 callback
  • Non-blocking HF ckpt uploader
  • Discord live-eval poster with comparison + history tables
  • BenchCAD/benchcad-easy renderer (cadquery → 4-view 268×268)
  • Multi-VM render orchestration (shard-aware, family-shuffle, looser tessellation tolerance)

License: see upstream cadrille.