Technical map of this repository: what each stage does, how data flows
between them, and what's planned next. AGENTS.md is the conventions/how-to-run
guide; this file is the "what is this and why is it shaped this way" guide.
This repository is a staged model-training workspace, split by pipeline stage rather than by model:
pre-training/ PDF corpus -> OCR text, summaries, layout, synthetic QA (CSVs)
fine-tuning/ text/summary pairs -> trained LoRA adapters
serving/ trained adapters -> inference (FastAPI)
training/ raw datasets -> from-scratch / non-LoRA trained models
Each leaf folder (pre-training/, fine-tuning/<pipeline>/,
serving/<pipeline>/) is an independent uv project: its own
pyproject.toml, uv.lock, .python-version, and pinned dependency set (in
particular, its own CUDA torch build). Nothing is shared at runtime between
folders — a pipeline can be deleted or reworked without touching its
siblings. One uv binary at the root drives all of them via
uv run --directory <folder> ... (see the root README.md's uv Commands
section for the current, verified list); there is deliberately no root-level
Python project or shared virtualenv, since the folders pin conflicting
dependency versions (e.g. different torch builds) that a single shared
resolution would fight.
Repo-wide hygiene is intentionally centralized rather than per-folder: one
.gitignore at the root (unanchored patterns match every project's drop-zone
folders at any depth), and one AGENTS.md at the root covering
conventions for the whole repo. No subfolder should have its own copy of any
of these.
Every project pins .python-version to 3.12. Without it, uv run picks
the newest CPython it can find (e.g. 3.14), and some pinned dependencies —
pillow==10.4.0 in particular — have no prebuilt wheel for that new a
version yet, so uv falls back to a from-source build that fails on Windows
(missing zlib headers). Pinning 3.12 (already installed and known to have
wheels for every pinned dependency across all four projects) is what makes
uv run --directory <folder> ... work reproducibly from a clean checkout.
This was verified by reproducing the failure and fixing it during this pass —
see the Verified working list below.
Turns a PDF corpus into training data. Local, GPU-first, Surya OCR +
Gemma 3 (unsloth/gemma-3-4b-it, an ungated mirror — no HF_TOKEN needed).
Five steps (exec_1.bat … exec_5.bat, or main.bat for an interactive
menu): PDF → PNG pages → OCR CSV → summary CSV / layout CSV / synthetic-QA
CSV, all written per-run to outputs/[timestamp]_[dataset]/.
Step 1 (PDF → PNG) is not a standalone Python entry point — it's
scripts/convert_pdf_to_png.ps1, which shells out to poppler
(pdftoppm/pdfinfo on PATH) and a small compress_png_max.py helper.
Run it via exec_1.bat or the .ps1 directly, not uv run ... python scripts/convert_pdf_to_png.py — that file doesn't exist (an earlier version
of this doc incorrectly assumed it did; fixed here). Steps 2–5
(ocr_detection_png.py, summarize_ocr_gemma.py, describe_layout_gemma.py,
generate_qa_gemma.py) are genuine argparse Python scripts and do run via
uv run --directory pre-training python scripts/<name>.py.
Status: out of scope for active work — left as-is, beyond the
.gitignore/.python-version consolidation described above.
Two example pipelines, same transformers+peft pattern, both LoRA-based,
both sized for a single RTX 3090 (24GB). (A third, Axolotl-based
axolotl-ocr-summary/ pipeline existed earlier but was removed by the repo
owner — it only resolved its uv environment on Linux/WSL, never natively
on Windows, since axolotl[deepspeed] depends on triton.)
| Pipeline | Framework | Base model | Data shape |
|---|---|---|---|
fine-tuning/vicuna-7b-lora/ |
transformers + peft (manual Trainer loop) |
lmsys/vicuna-7b-v1.5 — LoRA on q_proj/v_proj, loaded directly via AutoModelForCausalLM/AutoTokenizer. No LLaVA checkpoint, no vision encoder, no multimodal projector anywhere in the dependency graph. |
JSONL with text / summary fields |
fine-tuning/qwen25-3b-lora/ |
transformers + peft (same pattern as vicuna-7b-lora/) |
Qwen/Qwen2.5-3B-Instruct — LoRA on q_proj/v_proj (same target modules as Vicuna; Qwen2ForCausalLM uses the same separate Q/K/V/O naming, confirmed via peft's own default LoRA target-module table). ChatML prompt format instead of Vicuna's USER:/ASSISTANT: (verified against the tokenizer's chat_template/eos_token). |
JSONL with text / summary fields |
Naming/loading history: vicuna-7b-lora, previously llava15-lm-lora,
originally llava15-lora. Two renames, each fixing a real overstatement:
llava15-lora→llava15-lm-lora: the pipeline only ever LoRA'd the language-model backbone (q_proj/v_proj) ofllava-hf/llava-1.5-7b-hf— never the vision encoder or multimodal projector, never an image. The-loraname alone overstated that as a VLM fine-tune.llava15-lm-lora→vicuna-7b-lora: even loading LLaVA's checkpoint at all was unnecessary once the vision half was never used — it still downloaded the full ~14 GB multimodal weights viaLlavaForConditionalGeneration/AutoProcessorto get to a submodule that is, in substance, Vicuna-7B. This pass switched to loadinglmsys/vicuna-7b-v1.5directly viaAutoModelForCausalLM+AutoTokenizer— ~13 GB instead of ~14 GB, no vision-related code path in the dependency graph at all, same LoRA config/target modules. Trade-off:lmsys/vicuna-7b-v1.5is the checkpoint LLaVA 1.5 was later visually-instruction-tuned from, not the LLaVA-tuned weights themselves — different starting point, not directly comparable to the oldllava15-lm-lorarun's results, but a cleaner, smaller, honestly-named base for a language-model-only LoRA.serving/llava15-lorawas renamed toserving/vicuna-7b-lorain step, with matching model-loading changes (functionally required: a Vicuna-7B-trained adapter's parameter names don't matchLlavaForConditionalGeneration'slanguage_model.*prefix, so serving would fail to load it otherwise) — that serving folder has since been removed, see Stage 3. A plannedllava15-full-lorasibling, trained on image+text pairs and actually exercising the vision encoder/projector, remains the natural first real VLM fine-tune in this repo — that one should load the full LLaVA checkpoint. Full reasoning infine-tuning/vicuna-7b-lora/README.md.
vicuna-7b-lora/ is a generic text-summarization LoRA, not OCR-specific —
its interface, data source, and default prompt were all cleaned up this pass
to reflect that:
- JSONL field is
text(wasocr_text);build_vicuna7b_dataset.pyandgenerate_vicuna7b_lora.py's flags are--source-csv/--text/--text-file(were--ocr-csv/--ocr-text/--ocr-text-file). build_vicuna7b_dataset.pyonly builds from a CNN/DailyMail Parquet dump now (--cnn-dailymail-dir, required) — the earlier dual-source mode that also read pre-training's image-linked OCR/SUMMARIES CSV pair (normalize_image_key/resolve_image_path/load_summaries) was removed entirely, not just renamed, since it's not needed for this pipeline's current use (generate_vicuna7b_lora.py's--source-csvbatch-eval mode still accepts any generic CSV with atextcolumn, unrelated to that removed ingestion path).train_vicuna7b_lora.py/generate_vicuna7b_lora.py'sDEFAULT_INSTRUCTIONis now the CNN/DailyMail news-article wording (was "Summarize this scanned document page... UAP-related content") — since that's the only source this pipeline builds from,--instructionno longer needs to be passed explicitly for the common case.
qwen25-3b-lora/ is a near-clone of vicuna-7b-lora/ — same dataset
builder logic, same trainer/generator structure, same CLI shape. Two
verified differences (not assumed): the ChatML prompt wrapper (see table
above), and no protobuf/sentencepiece dependency needed (Qwen2.5-3B-Instruct
ships a ready tokenizer.json, unlike Vicuna's raw SentencePiece tokenizer).
No serving/qwen25-3b-lora/ — serving/ holds no pipelines at all now (see
Stage 3), so a serving folder would have to be written from scratch if this
adapter goes to production. It could not have been shared with the removed
serving/vicuna-7b-lora/ in any case: that one was Vicuna-specific, and the
ChatML wrapper differs.
Downloaded locally, outside the repo and outside the root folder (gitignored regardless). Measured against the actual files:
| Split | Rows | Size |
|---|---|---|
| train (3 shards) | 287,113 | ~772 MB |
| validation | 13,368 | ~35 MB |
| test | 11,490 | ~30 MB |
| Total | 311,971 | ~799 MB |
article (avg ~3,950 chars) → text, highlights (avg ~260 chars) →
summary. build_vicuna7b_dataset.py --cnn-dailymail-dir ... --max-samples 2000 was run end-to-end against the real files and produces valid JSONL
records; full details and commands in
fine-tuning/vicuna-7b-lora/README.md.
Before the switch to loading Vicuna-7B directly, a 2,000-sample run on the
old llava15-lm-lora pipeline (1,800 train / 200 val, 1 epoch, 450 steps,
~31 min on a single RTX 3090) showed loss dropping 1.66 → ~1.12 in the first
~50 steps then plateauing in a ~1.0–1.2 band, with eval_loss (1.11)
tracking train loss closely (no overfitting) — a normal curve for a
rank-16, 2-projection adapter on a small dataset, not evidence of a broken
run. That adapter and its data/hf_cache were deleted as part of this pass's
switch to lmsys/vicuna-7b-v1.5 (different base weights, not compatible
with the old adapter) — the numbers above are illustrative of the expected
curve shape, not a claim about the current pipeline's untrained state.
What carries forward: loss alone doesn't say whether summaries are actually
good, so generate_vicuna7b_lora.py has a --jsonl-eval <path> --num-samples N reconstruction-test mode (added the same pass as the old
run above) — it replicates the trainer's train/val split (same seed/ratio)
and prints genuinely held-out source/reference/generated triples with
token-F1, instead of requiring a manual --text string. Run against the old
adapter it produced coherent, on-topic, correctly-bulleted CNN/DailyMail
summaries (avg token-F1 0.357 across 5 samples) despite the plateaued loss —
this is the tool to use to judge the next real run on the current pipeline,
not the loss curve. See the
pipeline README.
One serving/<pipeline>/ folder per fine-tuning pipeline that has a serving
story. Currently empty. serving/vicuna-7b-lora/ — a FastAPI service
(app.py) that loaded the base Vicuna-7B model plus the trained adapter (or
a fused/merged model) once and served a JSON API plus a dataset-browser
front-end — was removed by the repo owner, the same way
fine-tuning/axolotl-ocr-summary/ was.
The design it demonstrated is still the intended shape for this stage, and
worth restating for whatever returns here: a serving folder is deliberately
decoupled from its fine-tuning counterpart, reading only that pipeline's
trained output directory (for Vicuna that was
../../fine-tuning/vicuna-7b-lora/runs/vicuna7b_lora/final_adapter) and
never importing its training code. That boundary is what makes a serving
folder independently deployable.
From-scratch / non-LoRA training of other models, as distinct from
adapting an existing checkpoint (fine-tuning/). Fifteen pipelines so far,
each an independent uv project and each writing out by hand whatever the
usual library would hide.
training/adult-income-logreg/ is
the first pipeline here: logistic regression on the UCI
Adult / Census Income
dataset, implemented with raw numpy rather than scikit-learn — the sigmoid,
binary cross-entropy loss, gradient derivation, and gradient-descent update
loop are all written out by hand in train_logreg.py so the math stays
visible, and build_income_dataset.py parses the raw CSV files and does
one-hot/z-score encoding without pandas. Own uv project like every other
pipeline folder, but no torch/CUDA dependency at all — just numpy.
Verified end-to-end against the real dataset: 30,162 train / 15,060 test
rows after dropping "?" rows (matches the cleaned-variant counts in
adult.names exactly), 300 epochs of batch gradient descent, 84.6% test
accuracy (in line with the 84–86% published for tree-based methods on this
same cleaned split — a from-scratch linear model landing close to that is
the expected sanity-check result, not a target to beat).
training/cifar10-vqvae/ is the next
pipeline here: a VQ-VAE
(van den Oord et al., 2017) trained
from scratch on CIFAR-10 (raw python-format pickles, parsed by hand
with stdlib pickle — no
torchvision/keras, no Pillow). It is the in-place successor of the
former cifar10-vae (renamed/converted): a plain VAE's blur comes from
Gaussian-posterior averaging in the ELBO, and VQ-VAE removes that
mechanism — an 8x8 grid of D-dim encoder vectors is replaced by its
nearest neighbors in a learned 512x64 codebook (straight-through
estimator, EMA codebook updates, commitment loss), and reconstruction is
driven purely by MSE. Same hand-written philosophy as the whole training/
folder: encoder/quantizer/decoder all plain torch.nn, torch only for
tensor ops/autograd/GPU, numpy-permutation batching, no VQ-VAE library,
no DataLoader. ~0.74M params (codebook included), ~40–60 min for 100
epochs on a single RTX 3090. Reconstruction-only by design (encode ->
quantize -> decode a real image back; no learned prior over the discrete
codes, so no sampling) — the discrete code grid is the substrate for a
later learned prior (the planned cascade). Its evaluator computes the same
metric suite used to judge the predecessor VAE (MAE/PSNR/SSIM/
high-frequency retention) plus a codebook-usage check, so the VQ-VAE vs
VAE comparison is reproducible; measured numbers in the pipeline README's
"Verified runs".
training/imdb-sentiment-cnn/
is the text-classification pipeline here: a Text CNN
(Kim, 2014) trained from scratch on
the Large Movie Review Dataset
(25k train / 25k test binary sentiment; raw review .txt files at
parsed by hand — no torchtext/datasets/nltk).
Same hand-written philosophy as the whole training/ folder: a
randomly-initialized trainable embedding (no GloVe — strictly
IMDB-only data by design), three parallel 1D convs (widths 3/4/5 × 128
filters) + ReLU + 1-max-pool per filter, concat, dropout 0.5, linear → 2;
torch only for tensor ops/autograd/GPU, numpy-permutation batching, no
DataLoader. ~6.6M params (6.4M in the embedding), 20 epochs in ~31 s
on a single RTX 3090, best checkpoint by val acc (peaks at epoch 3 before
the model overfits — train acc → 100%). Measured on the held-out 25k test
split: 89.2% accuracy (neg 88.96% / pos 89.43%) — well above Kim's
published CNN-rand (82.7%), which the README attributes to full-length
reviews plus val-based early stopping. A dropout-0.7 variant scored 87.9%
on test and was discarded. Full numbers in the pipeline README's
"Verified runs".
training/flow-matching-mnist/
is the newest pipeline here, and the first generative model family in
this repo that is not an autoencoder: flow matching / rectified flow
trained from scratch on MNIST. The model learns a velocity field
v(x,t) whose ODE transports N(0,I) at t=0 into the data at t=1;
training regresses that velocity on straight-line conditional paths with
plain MSE — x_t = (1-(1-sigma_min)*t)*x0 + t*x1, target x1 - (1-sigma_min)*x0 — which is Lipman et al.'s conditional-OT path
(2210.02747) and, at the default
--sigma-min 0.0, exactly the rectified flow of Liu et al.
(2209.03003). There is no noise
schedule, no variance parameterization, and no ELBO — that absence is the
point, and it is the difference between this and a DDPM. Same hand-written
philosophy as the rest of training/: the UNet velocity field (three
resolutions, one 7x7 self-attention block, sinusoidal time embedding as a
per-channel bias), the EMA, and the Euler/Heun ODE samplers are all plain
torch.nn; no diffusers/torchcfm/torchdiffeq/torchvision, no
DataLoader, numpy-permutation batching. ~1.18M params, 40 epochs in
~5.3 min on a single RTX 3090.
It is the deliberate counterpart to training/mnist-vae:
same dataset, same data/mnist.npz contract, same hand-written zlib PNG
writer, so the two prior-sample grids are directly comparable — the flow
model's are visibly sharper, the VAE's blur being the Gaussian-posterior
averaging that cifar10-vqvae also exists to remove. That comparison is
model-family vs model-family, not a controlled ablation: 1,175,841
params of UNet against 370,945 of plain conv encoder/decoder, and the two
runs cannot separate objective from capacity. The honest cost difference
they do show: the VAE generates in one forward pass, the flow model needs
20–50 network evaluations.
Judging it needed care, and two of this pipeline's guardrails came from
getting it wrong first. (1) The evaluator's round-trip MAE/PSNR sweep
measures ODE discretization error, not sample quality — the ODE is
time-reversible, so a real digit can be integrated back to noise and
forward again, but a near-zero velocity field round-trips perfectly since
the identity is its own inverse; a 2-epoch smoke run really did post a
better round-trip PSNR than the converged model. The sweep's actual use is
choosing --num-steps, and it shows Heun winning per network evaluation,
not just per step (10 Heun steps beat 50 Euler steps by ~2 dB at the same
100 evals). (2) The nearest-neighbour memorization check reports a distance
that is meaningless without a scale, so real held-out test digits are
measured against the training set the same way as a control. There is
deliberately no FID — it would require a pretrained Inception network,
against this folder's from-scratch rule, and a substitute number would be
worse than none. Measured figures in the pipeline README's "Verified runs".
training/rvq-audio-codec/ is the
newest pipeline here, and the first audio pipeline in the repo: a
neural audio codec with residual vector quantization (the
EnCodec/SoundStream/DAC architecture) trained from scratch on LJSpeech
(13,100 wavs, 23.92 h, parsed by a
hand-written RIFF/WAVE chunk walker - no torchaudio, no soundfile, no
librosa, no scipy). A SEANet-style strided conv encoder maps the waveform
to 68.9 frames/s, a stack of 8 codebooks x 1,024 entries quantizes each
frame (each codebook quantizing the residual the previous one left), and a
mirrored transposed-conv decoder reconstructs it - 5.51 kbps. 7,338,658
params, plus a 2,112,582-param multi-scale STFT discriminator that exists
only during training.
It is the deliberate successor of
training/cifar10-vqvae: that folder
has one codebook of 512 entries looked up in the full 64-dim latent, this
one stacks eight looked up in an 8-dim factorized projection under cosine
distance, with EMA updates and dead-code re-initialization. 9 bits per
latent position is enough for a 32x32 thumbnail and nowhere near enough for
a waveform; RVQ is how the bit budget is bought without a K^N codebook.
It is also the layer every modern audio LM (VALL-E, MusicGen, Moshi) sits
on - those models generate codec tokens, not waveforms.
Three things are deliberate and worth not undoing. (1) LJSpeech is 22,050
Hz and is trained at that native rate - no resampler is written, so the
frame rate is 68.9 Hz and the bitrate 5.51 kbps rather than EnCodec's
published 24 kHz / 75 Hz / 6 kbps. (2) Quantizer dropout (a random
n_q in [1, N] on half of each batch) is what makes one trained model
serve the whole 1->8 codebook ladder; without it the 1->8 quality demo
would need eight separate runs. (3) The discriminator is staged behind
--adv-start-step, because a randomly-initialized generator fighting a
randomly-initialized discriminator collapses a codec in the first thousand
steps; --lambda-adv 0 turns it off entirely for a reconstruction-only
A/B.
Judging it needs the same care as flow matching's ODE sweep. SI-SDR is a
weak proxy for a GAN-trained codec - the adversarial loss trades exact
waveform/phase alignment for perceptual realism, so a model that sounds
better can post a worse SI-SDR than a reconstruction-only one. The real
evaluation is the original_NN.wav / recon_nq{8,4,2,1}_NN.wav files the
evaluator writes (hand-written 44-byte RIFF writer, the inverse of the
builder's parser), plus the per-codebook usage table that says whether the
8th codebook is doing any work. There is deliberately no ViSQOL/PESQ/
NISQA - each needs an external binary or a pretrained network, the same
rule that keeps FID out of flow-matching-mnist.
The discriminator is also where the run's cost lives, by a wide margin.
Measured on the RTX 3090 at batch 32: reconstruction-only runs at 7.75
steps/s, and turning the discriminator on in fp32 drops that to 0.95 —
8x, because its spectrograms are much larger than the waveform they
judge (173 x 257 positions at the 512-point resolution against 22,080
samples, three resolutions, three passes per step) and those conv shapes
map badly onto fp32 tensor cores. cudnn.benchmark and TF32 matmul were
both measured and change nothing (0.90-0.95 steps/s, inside the noise).
What does work is bf16 autocast on the critic only (--disc-bf16,
default on): 2.01 steps/s and 10.8 GiB peak instead of 16.8, turning a
7-hour 60-epoch run into a 3.3-hour one with no architecture change. The
generator, the codebook lookup and every EMA update deliberately stay fp32
— bf16 EMA statistics would quietly stop accumulating small updates, which
is exactly the mechanism dead-code revival exists to detect.
One guardrail came from getting it wrong first: the dead-code cutoff is a
fraction of uniform codebook usage, not the absolute 2.0 that EnCodec and
vector-quantize-pytorch use. One batch here is 32 x 69 = 2,208 vectors
over 1,024 entries, so uniform usage is only 2.16 per entry and an absolute
2.0 condemns half a healthy codebook every sweep - the first smoke run
reported 1,023 of 1,024 entries "revived" per codebook; after the fix, 0.
training/fashion-mnist-dcgan/ is
the repo's first GAN pipeline: a DCGAN
(Radford et al., ICLR 2016) trained
from scratch on Fashion-MNIST (60k/10k, 28x28 grayscale, 10 classes, IDX
ubyte files parsed by hand - the Kaggle CSVs are also accepted by the
builder). Generator: z ~ N(0,I) (100-dim) -> linear -> 7x7x256 ->
BN+ReLU -> deconv -> 14x14x128 -> BN+ReLU -> deconv -> 28x28x1 -> Tanh.
Discriminator: strided convs 28 -> 14 -> 7 -> 3 -> 1 with LeakyReLU(0.2)
and BN everywhere except the input layer, ending in a single logit. Both
nets use the hand-written N(0, 0.02) initialization, one-sided label
smoothing (real = 0.9), and Adam at 2e-4 with betas (0.5, 0.999) - the
DCGAN tuning that makes the two-player game actually converge. Hand-written
like the rest of training/: no torchvision, no kagglehub/
pytorch-gan-metrics, no DataLoader (numpy-permutation batching).
Two notes worth keeping. First, 28x28 does not divide cleanly down
DCGAN's canonical 32x32 ladder: three stride-2 convs take 28 -> 14 -> 7
-> 3, so the discriminator's last feature map is 3x3 (a final 3x3 conv to
one logit) and the generator must start from a 7x7 grid, not 4x4. The
shapes in train_dcgan.py are the verified ones - "fixing" them to the
paper's 32x32 numbers breaks the tensors. Second, a GAN is judged by its
samples, not its loss: D/G losses move adversarially and say almost
nothing about sample quality, so train_dcgan.py writes a fixed-z sample
grid every --sample-every epochs (collapse becomes visible across
training) and evaluate_dcgan.py emits samples_grid.png plus the same
nearest-neighbour memorization guard as flow-matching-mnist (L2 to the
closest training image, compared against a real-image control) and a
pairwise-diversity probe. There is deliberately no FID/IS - both need
a pretrained Inception, the same rule that keeps FID out of
flow-matching-mnist and ViSQOL out of rvq-audio-codec. The pipeline is
new; verified-run numbers go in the pipeline README's "Verified runs" once
it has been run on the repo owner's RTX 3090.
training/vit-cifar10/ is the first
attention-based vision model in the repo — and its first from-scratch
transformer of any kind: a Vision Transformer
(Dosovitskiy et al., 2021, in the
pre-LN / norm-first layout popularized by DeiT) trained from scratch on
CIFAR-10. Hand-written philosophy like the rest of training/: the patch
embedding, learned CLS token + positional embeddings, the transformer
blocks, and the multi-head self-attention (QKV projections, scaled
dot-product, output projection) are all plain torch.nn — no
transformers/timm/torchvision, no DataLoader (numpy-permutation
batching). Flip+crop augmentation is plain torch ops (torch.flip,
zero-pad + random crop, per-channel normalize with the hardcoded CIFAR-10
train statistics). ~10.7M params at the defaults (--dim 384 --depth 6 --heads 6 --mlp-ratio 4), AdamW with weight decay 0.05 and a hand-written
linear-warmup-then-cosine LR schedule (warmup is the part ViTs need that
the rest of training/'s plain-cosine trainers don't), best checkpoint by
val acc, ~23 min for 60 epochs on the RTX 3090 fp32. The evaluator
reports test top-1/top-5, per-class accuracy + confusion matrix, and writes
a hand-written zlib RGB predictions_grid.png (first 32 correct, first 32
misclassified, green/red borders) — deliberately no pretrained-feature
score, the same rule that keeps FID out of flow-matching-mnist and
ViSQOL out of rvq-audio-codec. Verified on the RTX 3090 (repo owner's
run): 66.82% test top-1 / 97.32% top-5 at the 60-epoch defaults in
1,367 s (~23 min), 10,695,562 params, best val 67.50% at epoch 60,
frog best (81.4%) / cat worst (44.7%) with the classic cat↔dog and
truck↔automobile confusions. That is below the ~80–86% figure this entry
originally estimated — too optimistic for flip+crop-only at 60 epochs;
the measured 66.8% is the record (the correction is documented in the
pipeline README).
training/mae-cifar100/ is the repo's
first representation-learning (self-supervised) pipeline: a Masked
Autoencoder (He et al., 2022) trained
from scratch on CIFAR-100 — patchify → mask 75% → encoder → lightweight
decoder → MSE on the masked patches. The encoder is vit-cifar10's
patch-embed/block stack reused (copied in by hand; the pipelines don't
import each other), at a denser grid: default patch 2 → 256 patches (64
visible at 75% masking — the paper's regime at 32×32, unlike the sibling's
64-patch patch-4 config, which stays available via --patch-size). No CLS
token (MAE doesn't use one), no head; the decoder is a separate ~1M-param
transformer that exists only for pretraining. Masking is the paper's
fixed-count per-sample permutation, not a Bernoulli; the loss is MSE on
masked patches only with per-patch-normalized targets (--no-patch-norm
is the documented A/B of that trick). ~11.8M params total (10,750,848
encoder + 1,015,692 decoder), AdamW + warmup/cosine like the ViT sibling,
flip+crop on raw [0,1] pixels (no normalization in pretraining — the
reconstruction targets are the pixels), best checkpoint by a
deterministic full-image val reconstruction MSE. Judged by a hand-written
linear probe (linear_probe.py): a linear head trained from scratch on
the encoder's frozen, mean-pooled patch-token features (SGD momentum +
cosine, the paper's protocol) — the features are the model's own, so this
is a from-scratch evaluation that does not violate the no-pretrained-
features rule the way a FID/Inception score would. Verified on the RTX
3090 (repo owner's run): 25.56% test top-1 / 53.41% top-5 (coarse
top-1 37.9%) at the 60-epoch defaults in 2,166 s (~36 min). The
final (epoch-60) checkpoint is the record — it probes +0.91 top-1 over
the best-val (epoch-34) checkpoint (24.65% / 53.15% / 36.8%): val recon
bottomed at epoch 34 while the masked-MSE kept improving, so the late
features are better even though full-image recon drifted (a finding —
don't assume the best-val checkpoint holds the best representation).
Per-class oak_tree 69.0% best / bowl 1.0% worst — the classic CIFAR-100
pattern. Both figures are below the ~30–45% estimate this entry's
earlier draft carried — too optimistic for 60 epochs on 45k images
without probe-time augmentation; the measured 25.56% is the record (the
correction is documented in the pipeline README).
training/dit-cifar100/ is the repo's
first class-conditional generative transformer: a Diffusion Transformer
(Peebles & Xie, 2022 — the architecture
Sora is built on) trained from scratch on CIFAR-100, the natural big
sibling of training/flow-matching-mnist (same conditional-OT flow-matching
objective, now conditioned on the 100 real fine classes, with
classifier-free guidance). Hand-written philosophy like the rest of
training/: the patch embedding, the frozen 2D sincos positional
embedding, the adaLN-Zero transformer blocks, the hand-written multi-head
self-attention, the final unpatchify layer, the class embedding (+ null
token), the conditional-OT probability path, the velocity-regression loss,
the EMA, and the Euler ODE sampler are all plain torch.nn — no
diffusers/torchcfm/torchdiffeq/transformers/timm/torchvision,
no DataLoader (numpy-permutation batching). The objective is
mse(v(x_t, t, y), x1 - (1-sigma_min)*x0) on the conditional-OT path
x_t = (1-(1-sigma_min)*t)*x0 + t*x1 — byte-identical to
flow-matching-mnist's loss, plus the class. CFG is trained by dropping
the class to null token 100 with probability 0.1 (the DiT paper's value);
sampling is v_uncond + cfg*(v_cond - v_uncond) integrated by Euler.
Defaults --patch-size 2 --dim 256 --depth 8 --heads 8 = 9,828,876
params (256 tokens, the same density as DiT-S/4 at 256 px), AdamW +
warmup/cosine like the ViT siblings, flip+crop on raw [0,1] pixels (then
rescaled to [-1,1] like flow-matching-mnist), EMA weights for sampling,
best checkpoint by a deterministic-seed val velocity MSE, the 10k test
split stays unseen until evaluate_dit.py. Judged the
flow-matching-mnist way (the user's stated rule): samples_grid.png
(100 class-conditional samples, one per fine class), cfg_sweep.png (the
same latents at CFG scales 1.0–5.0), the nearest-neighbour memorization
check vs a real-image control, and the test velocity MSE — deliberately
no FID (pretrained Inception, same rule as everywhere else in
training/). Verified on the RTX 3090 (repo owner's run): 9,828,876
params, 60 epochs in 4,521 s (~75 min) — train velocity MSE 0.5174 →
0.1695, best val 0.1842 at epoch 55, test velocity MSE 0.1887 on the
held-out 10k (EMA weights, ~75 s/epoch at batch 256). Nearest-neighbour
check: generated samples sit ~47% farther from the training set (mean L2
12.105, min 9.572) than real unseen test images (8.210 / 5.152) — no
memorization, the same direction flow-matching-mnist measured. The
~0.17–0.19 loss floor is expected (the velocity target is irreducibly
random given (x_t, t, y)) — judge the sample grids, not the loss.
training/librispeech-speaker-id/
is the repo's first discriminative audio pipeline — every other audio
work here reconstructs a waveform; this one maps audio to a label — and its
first metric-learning objective: every trainer in training/ before it
is cross-entropy classification or MSE reconstruction, while this one learns
a space in which cosine distance between two 192-d vectors is the quantity
of interest. An ECAPA-TDNN
(Desplanques et al., Odyssey 2020) —
with the x-vector / TDNN it superseded
(Snyder et al., ICASSP 2018) available
as --arch xvector for a controlled A/B — trained from scratch on
LibriSpeech.
Hand-written like the rest of training/: the dilated conv blocks, the
squeeze-excitation channel attention, the Res2Net multi-scale channel split,
the attentive statistics pooling, the AAM-softmax head, the
speed-perturbation resampler, the EER/minDCF metrics and both plot renderers
are all plain torch.nn/numpy — no speechbrain/kaldi/sidekit/
pyannote, no torchaudio/librosa/soundfile/scipy, no DataLoader
(numpy-permutation batching over utterance indices, one crop each). The
log-mel filterbank is copied in by hand from
training/rvq-audio-codec (pipelines never import each other's code) and
works at 16 kHz unchanged because it takes sample_rate as an argument.
~2,939,616 params (--arch ecapa --channels 256 --mfa-dim 1536 --emb-dim 192, the AAM head's 44,352 more are discarded at inference);
--arch xvector is 2,589,140 at the same --channels 256 width, or
4,454,868 at --channels 512 (the literal TDNN width, and no longer a
like-for-like A/B). Both are trivial for 24 GB — the constraint is data and
time, not memory. Cost is linear in frames because there is no attention
over time and nothing recurrent, which is the whole reason a speaker encoder
trains in minutes where a Conformer-CTC on the same corpus needs 6–19 h per
epoch. Measured: 30 epochs in 7.0 min (ECAPA) / 5.3 min (x-vector) on
train.clean.100, and open-set EER 0.0725 / 0.0989 on 20 held-out
speakers.
The A/B's result is the finding, and it inverted the expected answer. On the val EER that selects the checkpoint, x-vector won (0.0095 vs 0.0162) and trained faster; on the held-out-speaker EER, ECAPA won (0.0725 vs 0.0989) — x-vector was fitting the 231 training speakers more tightly and transferring worse. A pipeline that selected and reported on the in-training split alone would have concluded the 2020 architecture is not worth its cost. This is the three-way split paying for itself, measured rather than asserted, and it implies the best-checkpoint criterion here is the wrong signal (it should be held-out speakers, not held-out utterances of seen ones). Don't "simplify" the split back to a boolean.
Things not to silently undo:
- The index carries a three-way split, not a boolean: 0 = train, 1 = val
(held-out utterances of seen speakers), 2 = unseen (held-out
speakers). Two different questions need two different held-out sets.
LibriSpeech is partitioned by speaker and
train.clean.100's 251 speakers have zero overlap withtest.clean/dev.clean(40 speakers each, themselves disjoint — verified, not assumed), so a 251-way classifier cannot be evaluated on test-clean: the held-out set has to come from inside the training split. Collapsing this to one boolean silently makes one of the two numbers meaningless. - The headline metric is the open-set EER on split 2, not the closed-set
accuracy on split 1. The accuracy split also selected the checkpoint, so
it is mildly optimistic;
eval_metrics.txtsays so in the file itself. Don't promote the accuracy to the headline number. - AAM-softmax (
--margin 0.2 --aam-scale 30) is what makes the embedding a metric. A plain softmax head can score well on 251-way identification while producing embeddings that are useless for anyone outside those 251 people, because nothing constrains the geometry. Don't swap it for plain cross-entropy to "simplify". - The head runs in fp32, outside the bf16 autocast. Its
sqrt/where/one_hotpath is not autocast-safe, and at ~5k parameters it costs nothing. Don't extend the autocast over it. - Evaluation crops are centred and deterministic, not random —
rng=Noneinsample_batch. A random crop at eval time makes the val EER jitter by more than the training signal. (This was got wrong first: the first run crashed withrng.integerson aNone, which is how the distinction got made explicit rather than implied.) - Speed perturbation is a resampler, and it is documented as one. It is
augmentation, not dataset resampling —
rvq-audio-codec's "no resampler" rule is about not resampling LJSpeech off its native 22,050 Hz, which this does not do (LibriSpeech is used at its native 16 kHz). Don't apply it to the val/unseen paths. ffmpegis a hard dependency of the builder, andpyarrowis a hard dependency of the pipeline. FLAC's residual coding is Rice-coded and bit-serial, so a pure-Python decoder over 57 GB is not slow, it is impossible; the precedent for shelling out to a format tool is alreadypre-training/exec_1.bat->poppler.pyarrowreads the parquet container, the same category as the stdlibpicklethe CIFAR pipelines parse. Don't hand-write either, and don't remove the ffmpeg check fromuv_setup.bat.- The FLAC decode is batched through ffmpeg's concat demuxer and sliced by FLAC STREAMINFO sample counts. One process per utterance would be ~28,500 spawns. The slice is asserted against ffmpeg's byte count per batch, so a mis-split fails loudly instead of silently shifting every offset after it.
- The builder verifies each split against its canonical LibriSpeech size
(28,539 for train.clean.100, etc.) — the same guardrail as
build_ljspeech_dataset.py's 13,100-wav check. Don't relax it. - There is deliberately no pretrained speaker-verification score — no
pyannoteembedding, no VoxCeleb-trained model to compare EER against. Same rule that keeps FID out offlow-matching-mnist, LPIPS out of the 3DGS branch and ViSQOL out ofrvq-audio-codec.
It is the sibling of training/rvq-audio-codec from the opposite direction:
that one reconstructs a waveform and knows nothing about who is speaking,
this one discards the waveform and keeps only identity. The natural join is
speaker-conditioned codec training, which is what would make
rvq-audio-codec multi-speaker. It is also the warm-up rung for a planned
CTC-ASR pipeline: same corpus builder, same memmap contract, same log-mel
front-end, same variable-length batching. Measured numbers in the pipeline
README's "Verified runs".
training/mamba2-tinystories/ is the
repo's first non-attention sequence model — every earlier sequence model
here is attention (ViT/MAE/DiT) or convolution (TextCNN/ECAPA), and this is the
first recurrence. A Mamba-2 selective state-space model in its SSD
(state-space-duality) form, trained from scratch on
TinyStories, with a
parameter- and token-matched causal transformer as --arch transformer
making the A/B a controlled comparison rather than two pipelines quoting
numbers at each other.
Hand-written like the rest of training/: the input-dependent discretisation
(dA = exp(dt*A), dB = dt*B), the chunked SSD scan, the causal depthwise
conv, the SiLU-gated output projection (no RMSNorm on the gate — the
paper's Mamba-2 block puts one there and this pipeline's does not, a documented
departure, so the two are not interchangeable), the tied-embedding LM head and a
byte-level tokenizer are all plain torch.nn — no mamba-ssm, no causal-conv1d, no
transformers/tokenizers, no DataLoader (numpy-permutation batching). The
corpus is a memmap (data/tinystories_train.u16 + an index .npz), the
rvq-audio-codec contract rather than an .npz of tokens: 1,943,728,852
tokens = 3.887 GB as uint16.
Three things are deliberate. (1) The tokenizer is the byte-level identity
(vocab 256), so one token is one byte, total_tokens equals the memmap size
exactly, and bits-per-token == bits-per-byte — which is what makes the A/B a
clean comparison. (2) The chunked scan is asserted against a naive
per-timestep reference (--selftest, run at startup): the naive form is an
oracle, not a second implementation to maintain, and the assertion is the
point. Measured fp32 forward relative error 3.8e-06 and fp64 6.6e-15;
independently reproduced against a clean-room oracle that agrees with
mamba2_scan_reference to exactly 0.0. That assertion earned its keep — it
exposed a transposed causal mask and an A/B/C argument swap during
development. (3) The builder's count guardrail refused to build at first,
and that is the most interesting result here: the canonical TinyStories figures
(2,119,718 / 21,989) are separator counts, not story counts. Train has
2,119,719 spans, 230 of them empty, giving 2,119,489 stories; valid has 21,990
spans, none empty. The naive stories = separators + 1 assertion failed loudly
rather than quietly dropping 230 entries, and the index now carries
train_separators, train_empty_spans, train_stories, … so the accounting
is auditable without rescanning 2 GB.
Measured smoke A/B at 300 steps / 2.46 M tokens (500,000 params vs 497,024,
matched to 0.6%): mamba2 val 1.9208 vs transformer val 2.1656, both far
below the uniform-prediction level ln(256) = 5.545. (Those counts are
sum(p.numel() for p in model.parameters()), and they are also what the
checkpoint records as num_params. Summing state_dict() instead gives
532,768 / 529,792 — exactly +32,768 = 256 x 128 — because the tied embedding /
LM-head weight is stored under two keys. An earlier draft of this entry quoted
the state_dict figure; use the deduplicated one.) That is a smoke A/B and
the README says so — ~0.5 M params at that budget cannot separate two
architecture families, which is exactly the lesson
librispeech-speaker-id's split-dependent reversal taught.
The documented config has since been run to completion on an idle RTX 3090, both legs, 1,104 steps x 32,768 tokens = 36.2 M tokens (1.91% of one pass): mamba2 3,515,008 params, 379 s (0.343 s/step), best val loss 0.8004 (ppl 2.23, 1.1547 bits/byte); transformer 3,527,488 params (+0.355%), 235 s (0.213 s/step), val 1.0053 (ppl 2.73, 1.4503 bits/byte). The evaluator re-scores the checkpoints at 0.8197 vs 1.0002 — mamba2 ahead by 0.18 nats, with the transformer 1.6x faster per step — and the length-extrapolation table is the sharper result: past the trained L=512 the transformer's learned absolute positional embeddings (linearly interpolated) degrade to 1.63 nats at L=1024 and 2.49 at L=2048 while mamba2 stays at 0.91 / 0.85. The MQAR recall probe is at chance for both (≈0.002, chance 0.0020) and is reported that way. All of it remains not a finding: 1.91% of a pass cannot separate two architecture families, and the contended s/step figures the earlier draft extrapolated from (0.53–0.81 s/step) turned out 1.6–2.4x pessimistic against the idle measurement above.
training/meanflow-cifar10/ is the
repo's first 1-NFE generative model: MeanFlow — average-velocity flow
matching (Geng, Deng, Bai, Kolter, He 2025)
— trained from scratch on CIFAR-10. It is the direct successor of
flow-matching-mnist and dit-cifar100: the same conditional-OT path, but
regressing the average velocity u(z,r,t) over an interval instead of the
instantaneous one, so a single network evaluation generates a sample where
its two siblings need 20–50. Hand-written: the class-conditional DiT block
stack, the two time embeddings (t and r), the adaLN conditioning, the
stop-gradient target, the EMA and the sampler; the JVP is torch.func.jvp,
i.e. torch's own forward-mode autodiff, not a library method. No
diffusers/torchcfm/torchdiffeq.
This entry exists mostly to record one bug, because the bug is the
interesting part. The first version of the objective used the model's own
output for the v in the target — v = u_theta(z_t, t, t) — which makes
u_theta == 0 an exact global optimum: if u=0 then v=0, the JVP=0, the
target=0 and the loss=0. It is a self-consistent fixed point that is not the
answer. Measured on a 2-D toy: that version diverges at lr 2e-3, and at lr 1e-4
"converges" to a model whose sample MMD (0.0647) equals raw noise's (0.0666) —
it learned nothing while the loss fell. The paper's Eq. 9–11 is explicit that
the target's only ground-truth signal is the conditional velocity
v_cond = x1 - x0, and with that the same toy trains and 1-NFE sampling works
(MMD 0.0067, ~10x closer than noise). Two diagnostics are therefore kept as
evidence rather than commentary: at initialization the raw MSE must be O(1)
— 6.33 with v_cond against 6.3e-04 with the model's own output — and
forcing r == t must reduce exactly to plain Flow Matching, measured
identical to 1e-9 (both 3.19850540). The warning generalises beyond this
pipeline: a wrong-but-smooth target still produces a falling loss, so "the loss
went down" is not evidence that an objective is right. The same wrong v was
left behind in evaluate_meanflow.py, where it made the reported "training
objective" read 0.0000 — a self-referential zero that any model scores,
including a random one, and which therefore reads as "solved". Corrected to
v_cond, the same smoke checkpoint reports 1.2536, exactly ||v_cond||^2
for CIFAR-10 in [-1,1]: with u ~ 0 the Jacobian ~ 0, the JVP term vanishes and
the average-velocity MSE must equal the instantaneous one, so the two metrics
agreeing exactly is the predicted signature rather than a coincidence.
Judged the flow-matching-mnist way — the sample grid and the NFE sweep over
SAME latents at each step count, plus the nearest-neighbour memorization check
against a real-image control — with deliberately no FID. Smoke checkpoint
(--dim 64 --depth 2, 176 steps = one full 45k-image epoch, 27.2 s on an idle
3090 against 64.4 s while the sibling pipelines held the GPU): train
average-velocity MSE 1.333 → 1.036, test 1.0072 average-velocity / 0.7566
instantaneous, and the non-degeneracy check passing on real data — the
at-init raw MSE came out at 1.99090 against ||v_cond||^2 1.99090, a ratio of
exactly 1.0000, and r == t reduces to plain Flow Matching to 1e-9.
Two findings from the end of that work are worth carrying forward. First, the
siblings' 1000x time-embedding scale is unusable here: flow-matching-mnist
and dit-cifar100 both scale the sin/cos time argument by 1000, but MeanFlow's
loss contains (t-r) * d/dt u, so that factor sits inside the objective and
multiplies the very time-derivative being learned — measured after one epoch,
scale 1000 diverges (1.32 → 35.09 while |(t-r)*dudt| grew 0.000 → 2.017 and
|v_cond| held at 1.164), while 100/10/1 give 1.014/0.821/0.812. The default is
now --time-scale 100.0, with the learning rate at 1e-4 rather than
dit-cifar100's 1e-3. Second, and more important for how this entry is read:
the 1-NFE claim is still unmeasured on CIFAR-10. The smoke checkpoint's
NFE=1 vs NFE=50 agreement is 0.038 RMS, but its samples have a pixel standard
deviation of only 0.315 against ~1.0 for real CIFAR-10 — a near-constant field
makes one step and fifty steps agree trivially, so the evaluator prints that
control beside the agreement figure. The pipeline's first real run (the
fast --dim 128 --depth 4 config, 20 epochs / 7,040 steps in 1,258 s:
train average-velocity MSE 1.2476 → 0.6458, test 0.6705 average-velocity /
0.2210 instantaneous, NN check 9.613 generated vs 9.080 real) does not change
that: its NFE=1 samples still carry a pixel std of only 0.1746 against ~1.0,
so its NFE=1 vs NFE=50 agreement of 0.15897 is not evidence either. 1-NFE holds
on the 2-D toy (MMD² 0.0139 against a 0.0655 noise reference). Settling it on
CIFAR-10 needs the --dim 256 --depth 6 60-epoch default, which is now measured
at ~10.2 h (0.867 s/step at batch 64; batch 256 does not fit the 24 GB card —
24,259 MiB observed) and has not been run.
training/3dgs-nerf-synthetic/ is the
repo's first 3D / novel-view-synthesis pipeline: 3D Gaussian Splatting
(Kerbl et al., SIGGRAPH 2023) trained from
scratch on the NeRF synthetic scenes, with a hand-written vanilla NeRF
(Mildenhall et al. 2020, train_nerf.py)
as the representation-vs-representation baseline — an explicit, rasterised
scene against an implicit, ray-marched one. Hand-written like the rest of
training/: the PNG decoder (stdlib zlib + the five filter types), the
camera conversion, the tile-based differentiable rasterizer, SSIM, the
densification/pruning and the plot renderers — no nerfstudio, gsplat,
diff-gaussian-rasterization, tiny-cuda-nn, plyfile, Pillow,
torchvision, no DataLoader.
The data contract has more sharp edges than any earlier pipeline, and each one was verified rather than assumed:
- The shipped PNGs are 8-bit RGBA (colour type 6), not RGB, and carry straight alpha. Verified bit-exact (max diff 0) against an independent decoder on 10 images across 5 scenes, exercising all five filter types.
- train, val and test are three different camera orbits that reuse the same
filenames (
train/r_0,val/r_0,test/r_0), so frames are keyed by(split, file_path)and never by filename. Verified at the split boundaries: frame i is bit-exactly frame i's image and carries the correct world-to-camera, so the keying is demonstrated rather than asserted. test/holds 600 PNGs whiletransforms_test.jsonlists 200 — the other 400 are depth and normal maps. Read the JSON; never glob the directory.- The camera chain is pinned by two invariants: the world origin projects to the image centre (< 0.05 px over all 8 scenes × 3 splits), and a silhouette carve of the alpha masks keeps a coherent object volume — the latter being what catches a roll error, which no geometric invariant sees.
- The tile rasterizer is asserted against a brute-force composite whenever
the top-K candidate bound is inactive, forward and gradients:
--self-checkreports forward 0.000e+00 (bit-identical) and gradient 1.492e-13 in fp64. It was worth having: it caught a wrong tile-scatter reshape that produced a factor-scale 0.5 error. The top-K bound and the 3-sigma bounding-box cull are deliberate approximations, and the unculled difference is reported, not asserted on — inventing a pass/fail threshold for an algorithm property would be the same mistake as adding a substitute perceptual score. - A pure-PyTorch rasterizer cannot run at 800×800, so the default is
--downscale 2(400×400); the published 3DGS figures are at 800×800 and are not comparable. There is deliberately no LPIPS, the rule this file already stated for the 3DGS branch before it existed.
The NeRF baseline reuses SceneData, psnr and ssim from train_3dgs.py,
so both models are scored by identical code, and it is coarse sampling only —
the paper's two-stage hierarchical sampler is not implemented, which is its
biggest quality gap. Measured smoke run: 530,052 params, 300 steps in 0.5 min,
loss 0.180 → 0.022, val PSNR 16.04 dB. That number is only meaningful
against the trivial baselines, which were measured for the same reason the
generative pipelines carry a nearest-neighbour control: a constant white
image scores 9.27 dB and a constant per-pixel train-mean image 13.55 dB, so
the NeRF beats the strongest deployable constant predictor by 2.5 dB after
thirty seconds. The 3DGS leg reached 16.959 dB val at iteration 200 (5000 →
5116 Gaussians, densification firing) before a densification-index crash
interrupted that smoke run.
The first completed 3DGS run (2026-09-23, idle RTX 3090) is lego, 5,000
steps at --downscale 4: 48.6 ms/step, 242.9 s, 5,544 Gaussians, best val
PSNR 19.748 dB, and held-out test 19.449 dB / SSIM 0.7941 over all 200
test frames with a train-vs-test gap of −0.256 dB (no overfitting). It also
found a real bug: reset_opacity() clamped opacities up to --min-opacity
(0.005) — the same constant the prune pass tests — instead of the reference's
min(opacity, 0.01), so every reset parked weak Gaussians exactly on the prune
line and the next densify pass deleted them en masse (2,628 of 2,992 = 87.8%
at the step-3,000 reset of a --downscale 2 run, which was abandoned; the
pre-fix 5,000-step run lost 1,023 of 5,830 at step 1,600). Fixed with a
lowering reset at --opacity-reset-value (default 0.01) plus a main() guard
refusing --opacity-reset-value <= --min-opacity; on the identical config the
fix is worth +0.67 dB test PSNR / +0.014 SSIM (18.779 → 19.449). Still not
the paper's budget: 5,000 steps at 200x200 is not 30,000 at 800x800, there is no
--downscale 1 run, and only lego has been trained.
None of the fine-tuning pipelines ship data — DATASET/, data/, runs/,
output/ etc. are all git-ignored, drop-zone folders (via the single root
.gitignore). Both vicuna-7b-lora/ and qwen25-3b-lora/ train on
CNN/DailyMail. Example small, permissively-licensed public datasets are
listed in the root README.md's Datasets section.
Two more drops were added to the local dataset folder for the newest work, both downloads rather than checked-in data:
- TinyStories (
roneneldan/TinyStories, both the V1 and the V2-GPT4 text corpora) fortraining/mamba2-tinystories. The V1 train file is 1,924,281,556 bytes and the valid file 19,447,282; the canonical published counts are separator counts (2,119,718 / 21,989), which is why the builder asserts spans and empty spans rather than the raw marker count — see themamba2-tinystoriessection above. - NeRF synthetic (the Blender-rendered
nerf_syntheticdrop, all 8 scenes) fortraining/3dgs-nerf-synthetic: 8 x (100 train / 100 val / 200 test) 800x800 RGBA PNGs plus depth and normal maps, built into a 5.72 GB uint8 memmap store (metadata.json+dataset_summary.txt) rather than an.npz— 3,200 x 800 x 800 x 3 is not something to hold in RAM. It ships no COLMAP points, which is why the 3DGS leg uses the paper's random-init fallback. - PASCAL VOC 2007 (the official Oxford
VOCtrainval_06-Nov-2007.tar,VOCtest_06-Nov-2007.tarandVOCdevkit_08-Jun-2007.tar), downloaded and count-verified — 9,963JPEGImages, 9,963Annotations, 632 segmentation masks, andImageSets/Main/train 2,501 / val 2,510 / trainval 5,011 / test 4,952. No pipeline uses it yet; it is staged for a detection branch, andVOCtest_06-Nov-2007.tardoes include the test annotations, so a genuine held-out mAP on the real test split is computable when that branch lands.
training/rvq-audio-codec is the one pipeline whose prepared data is too
large for the .npz contract the others share: LJSpeech is 3.80 GB of
int16, which becomes 7.6 GB as float32. It writes a raw
data/ljspeech_audio.i16 opened with np.memmap plus a small
data/ljspeech_index.npz of offsets/lengths/ids/split, and converts crops
to float one batch at a time. Both are covered by the root .gitignore's
data/ rule like everything else.
2,000-sample JSONL (1,800 train / 200 val), 2 epochs, 900 steps, ~61 min on
a single RTX 3090. Train loss 1.76 → 0.99, eval_loss essentially flat across
epochs (1.100 → 1.094 — a mild overfitting signal in isolation, train loss
kept falling while eval_loss didn't). What matters: reconstruction-test avg
token-F1 rose from 0.357 (an earlier 1-epoch/1,800-sample run on the
predecessor llava15-lm-lora pipeline) to 0.467 on this run, and the
generated summaries reproduced exact figures from source text correctly
(e.g. "383-41" and "70-26" vote counts). Confirms the earlier lesson again:
eval_loss plateauing is not itself a stop signal — the reconstruction test
is what actually shows whether a further epoch helped.
uv run --directory <folder> python <script> --help, and further real
executions where noted, actually run, not assumed:
-
training/cifar10-vqvae(successor of the formercifar10-vae) — real runs on the RTX 3090 against the actual downloaded CIFAR-10 python pickles:build_cifar10_dataset.pywrotedata/cifar10.npz(50k train / 10k test);train_vqvae.pyandevaluate_vqvae.pyrun end-to-end (100 epochs, ~10–15 min, codebook perplexity ~404/512, 512/512 codes fired on test — no collapse). Measured reconstruction on the held-out test set: PSNR 25.2 dB, SSIM 0.884, MAE 0.042, high-frequency detail kept 73.8% — vs the predecessor VAE's best (PSNR 21.5 dB, SSIM 0.742, HF 51.4%), i.e. the discrete-codebook family removes the plain-VAE blur mechanism. Full numbers in the pipeline README's "Verified runs". -
training/flow-matching-mnist— real run on the RTX 3090 against the actual MNIST IDX files:build_mnist_dataset.pywrotedata/mnist.npz(60k train / 10k test);train_flow.pytrained 1,175,841 params for 40 epochs in 317.5 s (val velocity MSE 0.2263 → 0.1704, best at epoch 38 — train and val track each other throughout, no overfitting);evaluate_flow.pyscored 0.1687 velocity MSE on the held-out 10k and wrote all three PNGs. Reproduced in a second independent run of the same commands (312.1 s, best val 0.1705, test 0.1687) — expect the third decimal to move. Samples are clean, readable digits with a handful of malformed glyphs per 64. The ~0.17 loss floor is expected, not a defect:u = x1 - x0is irreducibly random given(x_t, t), so the MSE floors at that conditional variance and can never reach zero — judge the samples, the same lessonfine-tuning/vicuna-7b-lorataught about loss plateaus. Memorization check: generated samples sit farther from the training set (mean L2 4.029, min 1.858) than real unseen test digits do (3.611 / 1.169). -
training/rvq-audio-codec— full 60-epoch run on the RTX 3090 against the real LJSpeech drop.build_ljspeech_dataset.pyverified all 13,100 wavs (22,050 Hz / 16-bit / mono PCM, confirmed by reading the RIFF headers, not assumed) and wrotedata/ljspeech_audio.i16— 1,898,881,532 samples, 23.92 h, 3.80 GB — plus the index (12,838 train / 262 val; shortest utterance 1.11 s, so nothing is dropped by the 1.0014 s crop). The model is 7,338,658 params (3,659,936 encoder / 3,660,162 decoder / 18,560 RVQ) plus a 2,112,582-param discriminator. Training took 2 h 54 min for 24,060 steps at 10.8 GiB peak, and validation mel fell 7.313 → 3.075, still improving at the end. Evaluated on 64 held-out utterances (398.5 s), the bitrate ladder is monotone on all three metrics — 0.69 kbps: −4.84 dB SI-SDR / 3.968 mel; 1.38: −0.87 / 3.528; 2.76: +0.86 / 3.249; 5.51 kbps: +1.81 dB / 3.083 mel — all four rungs served by the same model, which is what quantizer dropout is for. The headline result is the codebooks: all 1,024 entries of all eight are used, and the deepest codebook has the highest perplexity of the stack (903.6, against the first's 792.0). A naive RVQ usually leaves the last codebooks nearly dead; dead-code revival did its work early (6,072 revivals in epoch 1, 205 by epoch 3) and then went silent from epoch 7 on. Judged by ear it is a working codec with an audible metallic edge, not a transparent one — the very low SI-SDR says waveform phase is only loosely tracked, which is the trade the adversarial loss makes. Supporting checks: the hand-written WAV writer round-trips through the hand-written parser at the 16-bit quantization floor (5.32e-05 against a floor of 3.05e-05), and throughput was profiled rather than guessed (see Stage 4). Full tables in the pipeline README's "Verified runs". -
fine-tuning/qwen25-3b-lora—build_qwen3b_dataset.py,train_qwen3b_lora.py,generate_qwen3b_lora.py, plus a real 40-sample smoke train against the actual downloadedQwen/Qwen2.5-3B-Instructweights, confirmingtrainable params > 0(LoRA genuinely attached toq_proj/v_proj) rather than trustingpeft's target-module table alone. -
fine-tuning/vicuna-7b-lora— real 2,000-sample/2-epoch training run (see above), executed by the repo owner, not just a smoke test. -
training/vit-cifar10— full 60-epoch run by the repo owner on the RTX 3090 against the real CIFAR-10 drop:build_cifar10_dataset.pywrotedata/cifar10.npz(50k/10k);train_vit.pytrained 10,695,562 params in 1,367 s (~23 min) (train acc 0.30 → 0.714, best val 67.50% at epoch 60);evaluate_vit.pyscored 66.82% test top-1 / 97.32% top-5 on the held-out 10k and wrotetest_metrics.txt+predictions_grid.png. The ~80–86% expectation this pipeline's docs originally carried was corrected to the measured 66.8% (too optimistic for flip+crop-only at 60 epochs) — see the pipeline README. -
training/mae-cifar100— new pipeline (MAE on CIFAR-100, the repo's first self-supervised run), full 60-epoch run by the repo owner on the RTX 3090 against the localE:\datasets\cifar-100-pythondrop:build_cifar100_dataset.pywrotedata/cifar100.npz(50k train / 10k test, verified exact counts, fine + coarse labels);train_mae.py(11,766,540 params: 10,750,848 encoder + 1,015,692 decoder, patch 2 → 256 patches, mask 75%) trained for 60 epochs in 2,166 s (~36 min) (train masked-MSE 0.666 → 0.257, best val recon 0.47463 at epoch 34);linear_probe.pyscored 25.56% test top-1 / 53.41% top-5 (coarse 37.9%) on the held-out 10k using the final epoch-60 checkpoint — which probes better than the best-val epoch-34 one (24.65% / 53.15% / 36.8%): val recon bottomed mid-run while the masked-MSE kept improving, so the late features are the better representation (both probe metrics preserved inruns/; the comparison is deterministic — re-running the best-checkpoint probe reproduces it exactly) — and wrotetest_metrics.txt+probe_grid.png. A 2-epoch smoke preceded the real run (loss decreasing, grids/checkpoints written, ~35 s/epoch). -
training/dit-cifar100— new pipeline (class-conditional DiT on CIFAR-100, the repo's first class-conditional generative transformer), full 60-epoch run by the repo owner on the RTX 3090 against the same localE:\datasets\cifar-100-pythondrop:build_cifar100_dataset.pywrotedata/cifar100.npz(50k train / 10k test, verified exact counts, fine + coarse labels);train_dit.py(9,828,876 params, patch 2 → 256 tokens, dim 256 / depth 8 / heads 8) trained for 60 epochs in 4,521 s (~75 min) (train velocity MSE 0.5174 → 0.1695, best val 0.1842 at epoch 55);evaluate_dit.pyscored 0.1887 velocity MSE on the held-out 10k (EMA weights) and wrotesamples_grid.png(100 classes, one per fine class, cfg 3.0),cfg_sweep.pngandnearest_neighbours.png. Nearest-neighbour check: generated samples sit ~47% farther from the training set (mean L2 12.105, min 9.572) than real unseen test images (8.210 / 5.152) — no memorization, the same directionflow-matching-mnistmeasured. The ~0.17–0.19 loss floor is the irreducible conditional variance of the velocity target, not a defect. A 2-epoch smoke + a 1-epoch timing run preceded the real run. -
training/mamba2-tinystories— new pipeline (Mamba-2 SSD on TinyStories, the repo's first non-attention sequence model).build_tinystories_dataset.pyverified the canonical separator counts and wrote the memmapped corpus (1,943,728,852 uint16 tokens = 3.89 GB; 2,119,489 train / 21,990 valid stories); the fp32 scan self-test passed at 4.349e-04 abs / 3.797e-06 relative (fp64 6.576e-15). Both legs of the documented config were run to completion on an idle RTX 3090: mamba2 (3,515,008 params) 1,104 steps in 379 s, best val loss 0.8004 (ppl 2.23, 1.1547 bits/byte); the matched transformer (3,527,488 params, +0.355%) 1,104 steps in 235 s, val 1.0053 (ppl 2.73, 1.4503 bits/byte).evaluate_mamba.pyre-scored them at 0.8197 vs 1.0002 and showed the transformer's learned absolute positional embeddings collapsing past its trained length (1.63 nats at L=1024, 2.49 at L=2048, against mamba2's 0.91 / 0.85), while the MQAR recall probe stayed at chance for both legs (≈0.002 against 0.0020). 36.2 M tokens is 1.91% of one pass, so none of it is a finding about the two architectures — see the pipeline README's §3b. -
training/meanflow-cifar10— new pipeline (MeanFlow, the repo's first 1-NFE generative model).train_meanflow.py --dim 128 --depth 4 --num-epochs 20 --batch-size 128(1,356,556 params) trained 7,040 steps in 1,258 s (21.0 min); train average-velocity MSE 1.2476 → 0.6458, best val 0.6715 at epoch 19.evaluate_meanflow.pyscored test 0.6705 average-velocity / 0.2210 instantaneous on the held-out 10k and wrotesamples_grid.png(100 class-conditional 1-NFE samples),nfe_sweep.png(the same latents at NFE 1/2/4/10/50) andnearest_neighbours.png(generated mean L2 9.613 vs real test 9.080 — no memorization). The 1-NFE claim is still unmeasured on CIFAR-10: the samples' pixel std is 0.1746 against ~1.0 for real CIFAR-10, so the 0.15897 NFE=1-vs-50 agreement is not evidence. The documented 60-epoch--dim 256 --depth 6default measures ~10.2 h (0.867 s/step at batch 64; batch 256 exceeds the 24 GB card) and was not run. Not executed this pass (no PDFs/poppler set up in this environment): -
pre-training/exec_1.bat/scripts/convert_pdf_to_png.ps1
fine-tuning/vicuna-7b-lorahas a verified-good real run (see above) — reasonable next moves are more samples (the eval_loss plateau suggests more epochs on this same 1,800-row set has limited further upside), judged by the reconstruction test, not loss alone.fine-tuning/qwen25-3b-lorais smoke-tested but not yet trained for real — same next step as Vicuna's first run: build a few-thousand-sample JSONL, train, then judge with--jsonl-eval.training/has a verified realcifar10-vqvaerun (see above) — the planned next rung is stage 2 of its cascade: a learned prior over the discrete code grid (PixelCNN/transformer over code indices, or a latent DDPM), latent-diffusion style.training/imdb-sentiment-cnnis verified at 89.2% test acc with random embeddings in31 s — natural next rungs: a GloVe variant (+1–3 pts expected), or the bigger from-scratch projects that use the 50k unlabeled reviews (AWD-LSTM LM-pretrain + fine-tune, or a small transformer with MLM pretraining, both ~91% territory).training/flow-matching-mnistis verified at ~5.3 min for 40 epochs and was still improving when it stopped — natural next rungs: more epochs or a wider--base-channels; class-conditioning plus classifier-free guidance (the smallest real upgrade, and what makes samples steerable); a second rectification pass (re-train on the model's own noise/sample pairs) to straighten the paths for 1–4-step sampling, which is the whole reason rectified flow is used in production; or the same objective on CIFAR-10 next tocifar10-vqvae, where the VAE-blur comparison has more room to show itself than at 28x28.training/rvq-audio-codecis trained and verified (see above) — the audible gap left is the metallic edge, which the very low SI-SDR (+1.81 dB) identifies as loose phase tracking, and which is a training-budget problem rather than an architecture one: EnCodec and DAC run several hundred thousand steps against this run's 24,060. Natural next rungs, in rough order of value: the reconstruction-only A/B (--lambda-adv 0) to measure what the discriminator is actually worth; the collapse-mitigation ablations (--code-dim 128,--vq-l2-normalize 0,--dead-code-threshold 0,--vq-mode loss), whose findings are meant to feed back intotraining/cifar10-vqvae's single codebook; and then the obvious sequel — an autoregressive prior over the RVQ code indices, which is what turns a codec into an audio LM and is the same "learned prior over discrete codes" rung already planned forcifar10-vqvae. Multi-speaker (LibriTTS/VCTK) is the fix for the single-speaker limitation, but only if generalization becomes the goal.training/vit-cifar10is verified at 66.8% test top-1 / 97.3% top-5 (60-epoch defaults, ~23 min on the RTX 3090; best val 67.50% at epoch 60, still slowly improving at the end) — natural rungs, in rough order: the--no-augmentA/B (still unmeasured; measures how much of the accuracy is the flip+crop); a longer run (--num-epochs 120, the cosine schedule is designed for the full budget); stronger hand-written augmentation (AutoAugment-style ops are the biggest known lever on CIFAR-10 ViTs); a deeper/wider variant (--depth 8 --dim 512, ~24M params, still comfortable on 24 GB). The planned sequel this pipeline's patch-embed/block stack was built for — self-supervised pretraining — has now landed as the siblingtraining/mae-cifar100(mask-reconstruct MAE rung, same stack reused); the next rung after that is the I-JEPA-style pipeline (small ViT encoder + predictor + EMA target on STL-10/Tiny ImageNet, linear-probe eval), which shares MAE's mask-reconstruct DNA.training/mae-cifar100is verified at 25.56% test top-1 / 53.41% top-5 (coarse 37.9%; ~36 min for the 60-epoch pretrain) using the final epoch-60 checkpoint — it probes better than the best-val epoch-34 one (24.65% / 53.15% / 36.8%), because val recon bottomed mid-run while the masked-MSE kept improving (don't assume the best-val checkpoint holds the best representation). Natural rungs, in rough order: the--no-patch-normA/B (measures the MAE trick); the--patch-size 4A/B (vit-cifar10's literal 64-patch grid at the same 75% mask — the denser patch-2 default is expected to win, but it's unmeasured); a--mask-ratiosweep (50/75/90 — the paper's 75% is tuned for 196 patches, not 256); more pretraining epochs (--num-epochs 120, the cosine is designed for the full budget); probe-time augmentation or a k-NN probe as an augmentation-free alternative; and then the I-JEPA rung above, which this pipeline's mask-reconstruct machinery sets up directly.training/dit-cifar100is verified at ~75 min for the 60-epoch defaults (9,828,876 params, train velocity MSE 0.5174 → 0.1695, best val 0.1842 at epoch 55, test 0.1887; generated samples sit ~47% farther from the training set than real test images — no memorization). Natural rungs, in rough order: the--cfg-scalesweep on the final checkpoint (the evaluator'scfg_sweep.pngshows 1.0–5.0; the trade-off between class fidelity and diversity is the obvious knob to tune per class group); a longer run (--num-epochs 120, the cosine is designed for the full budget — val was still improving at epoch 55–60); the--no-augmentA/B (measures how much of the quality is the flip+crop); a bigger model (--dim 384 --depth 12, the literal DiT-S/2 sizing — ~29M params, still comfortable on 24 GB); and the natural sequel this pipeline's conditioning machinery sets up directly: class-conditional generation at higher resolution (CIFAR-10/STL-10 at 64×64, or latent-DiT oncifar10-vqvae's codes), which is what turns the Sora-style stack into a production-shaped generator.training/librispeech-speaker-idis a new pipeline with two verified runs (see above and the pipeline README). The--arch xvectorA/B is done, and its result inverted the expectation — x-vector won the val EER that selects the checkpoint and lost the held-out-speaker EER — so the highest value next rung is not a model change at all: select the best checkpoint on held-out speakers rather than held-out utterances of seen speakers, which is what the reversal says the current criterion gets wrong. Close behind it: a fourth held-out utterance split the trainer never touches (the builder writes three splits; the closed-set number is optimistic without a fourth). Then the ordinary knobs: the--no-speed-perturbA/B (how much of the result is the augmentation),--channels 512(the published ECAPA width, ~4x the compute), a--crop-secondssweep (2 s is the common choice, 3 s the other), and ECAPA's successors, which are the same trainer with different blocks. The higher-value rung beyond the model is the one this pipeline was built to unlock: the same corpus builder and log-mel front-end are what a planned CTC-ASR pipeline needs, so its real return is retiring that data risk — see the pipeline README's "Why this pipeline belongs here". Beyond that, a speaker-conditionedrvq-audio-codec(the embedding as a conditioning vector rather than a classification target) is the direct fix for that codec's documented single-speaker limitation, and is the first place this repo's two audio pipelines would meet.fine-tuning/llava15-full-lora(planned, not started): the first real VLM fine-tune in this repo — image+text pairs, vision encoder/projector actually in the training graph, unlikevicuna-7b-lora/qwen25-3b-lora.- A
phi35-mini-lorasibling (discussed, not started) would needtarget_modules=["qkv_proj"]instead of["q_proj", "v_proj"]— Phi-3 fuses Q/K/V into one linear layer (confirmed by readingPhi3Attention's source), so thevicuna-7b-lora/qwen25-3b-loratarget-module config would silently attach to nothing on that model.