YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
AdiVaani — Hindi ↔ Marathi Neural Machine Translation Hirinng Assignmnet
This repository contains four trained Hi → Mr translation systems built end-to-end from the assignment's parallel corpus, on a $40 GPU budget, with full reproducibility hashes, content-addressed checkpoints, and the literature-grounded rationale for every architectural and hyperparameter choice. The assignment is not BLEU-maximising — it grades engineering rigour, architectural reasoning, and honest failure analysis — and this repo is structured to reflect that.
Headline result
| System | Architecture | Test BLEU-100 | Test CHRF++-100 |
|---|---|---|---|
| P1-A | LSTM-Seq2Seq + Bahdanau, random embeddings | 0.00 | 0.00 |
| P1-B | LSTM-Seq2Seq + Bahdanau, L3Cube-BERT-v2 frozen | 2.11 (greedy) / 2.53 (beam-4) | 17.16 / 17.32 |
| P2-MT v2 Run A | Warm-started enc-dec (BERT-110M + GPT-124M), single-direction | (val.set)8.02 / (Test set)28.95 | |
| P2-MT v2 Run B | Warm-started enc-dec, bidirectional | 9.86 (greedy) | 32.74 (greedy) |
Best checkpoint: results/checkpoints/P2-MT-v2-bi_68534c48046d_best.pt
What's in this repo
| Top-level | What it holds |
|---|---|
README.md |
This file. |
Haq_Nawaz_Malik_MISN.pdf |
Technical Detailed report pdf file |
requirements.txt |
Pinned dependencies. |
.gitignore |
Excludes env etc, etc. |
modal_app.py |
All Modal entry points (~1 600 lines). One App("adivaani-mt") with 20+ functions across CPU/T4/A10G/A100 tiers, all with hard timeouts and persistent Volume. |
migrate_to_fallback.sh |
Helper bash script used for migration |
configs/ |
7 YAML configs, one per experiment. Every config is content-addressed (a config_hash drives the checkpoint filename); a one-byte edit breaks resume. |
src/ |
Library code (data, models, training, eval, utils). 100 % covered by unit tests. |
scripts/ |
CLI utilities: evaluation, qualitative tables, train-side metric computation, plotting, HF push helpers. |
tests/ |
103 unit tests covering RoPE math, GQA equivalence, attention masking, causal-mask leak checks, whole-word masking correctness, EMA, bidirectional dataset, parameter counts vs assignment targets. |
data/ |
Splits, tokenizer, manifests. Big train.* files gitignored; SHA-256 manifests committed. |
results/ |
(27GB) Checkpoints, logs (TSV), metrics (JSON), plots (PNG), translations (Markdown). |
Architecture summary
Part I — LSTM-Seq2Seq with Bahdanau attention
Same architecture across both runs; only the embedding source changes — this is the §3.2 transfer-learning hypothesis test.
src.hi → Embedding (V × D) → BiLSTM-2L-H512 ─┐
├→ Bahdanau additive attn → LSTM-2L-H512 (input feeding, Luong combine) → tied I/O → Marathi token
src state→ linear bridge → decoder hidden init ──┘
- P1-A (
configs/part1_random.yaml, hashae234580eb32, 35.8 M params): random Xavier embeddings, trainable, shared SP-BPE 16k vocab. - P1-B (
configs/part1_bert.yaml, hashcec203cdcc90, 425 M params): frozen L3Cube-BERT-v2 embeddings (per-language WordPiece, V=197 285).
Why this matters: P1-A still produced empty greedy hypotheses after 12 300 steps (3 epochs, $2.68). P1-B produced real translations at CHRF=16 in one effective epoch ($5.45).
Part II — Custom Transformer + warm-started encoder-decoder
All three Part II models share the same custom Transformer block:
| Component | Choice | desicion |
|---|---|---|
| Position encoding | RoPE base=10 000 | Relative-pos info multiplicatively; better length extrapolation than sinusoidal/learned |
| Attention | GQA 12 query → 4 KV heads | 3× KV-cache reduction at near-zero quality loss |
| Normalization | RMSNorm ε=1e-6 | Drops mean centering, ~10 % faster than LayerNorm |
| Activation | GELU (not SwiGLU) | Keeps param accounting clean against the 110 M / 124 M targets |
| Residual ordering | Pre-norm | Stable without warmup tricks |
BERT-like encoder (src/models/part2/bert_encoder.py, 106.75 M ≈ 110 M target):
12 layers × 768 hidden × 4096 FFN, GQA 12q/4kv, RoPE, tied MLM head. Pretrained with whole-word MLM (Devanagari-aware, 15 % WORDS, 80/10/10 mask/random/keep) for 20 000 steps on A100-40GB → val_perplexity 29.33 at step 19 000. NSP is removed per assignment §4.2.
GPT-2-style decoder (src/models/part2/gpt_decoder.py, 122.49 M ≈ 124 M target):
14 layers (+2 vs BERT to compensate for the smaller shared 16k vocab, × 768 hidden × 4096 FFN, same GQA/RoPE/RMSNorm. Trained on language-tagged concatenation of Hindi and Marathi monolingual sides → val_perplexity 28.69 at step 19 000.
Warm-started encoder-decoder (src/models/part2/warm_started_ed.py, 239 M total): pretrained BERT + pretrained GPT + newly-initialised cross-attention (GQA, no RoPE — relative position across enc/dec is undefined) inserted between self-attn and FFN in every decoder block. Token embeddings shared between encoder and decoder (saves ~12 M, possible because both pretrained on the same shared 16k vocab). 22 M trainable cross-attn parameters; the rest unfreeze in V2.
Reproduction — full recipe
If you have only a fresh machine and the GitHub repo:
# 1. Get the code
git clone https://huggingface.co/Omarrran/IITD_MISN_Hiring_Assignment_Haq_Nawaz/
cd folder #where you cloned
# 2. Python environment
python3.12 -m venv .venv && source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install -r requirements.txt
# 3. Verify
python -m pytest tests/ -q # expect 103 passed
# 4. Get the trained checkpoints from HF (instead of re-training)
hf auth login # paste read token
hf download Omarrran/IITD_MISN_Hiring_Assignment_Haq_Nawaz \
--include "data/**" \
--include "results/checkpoints/P2-MT-v2-bi_68534c48046d_best.pt" \
--include "results/checkpoints/P2-BERT_c5d7a375e5f5_best.pt" \
--include "results/checkpoints/P2-GPT_7ad68b7b3321_best.pt" \
--local-dir .
# 5. Inference example (Hindi → Marathi)
python -c "
import torch
from src.utils.config import load_config
from src.data.tokenizer import SPTokenizer
from src.models.part2.bert_encoder import build_bert_encoder
from src.models.part2.gpt_decoder import build_gpt_decoder
from src.models.part2.warm_started_ed import WarmStartedEncoderDecoder
tok = SPTokenizer('data/tokenizer/spm_16k_shared.model')
bert_cfg = load_config('configs/part2_bert_pretrain.yaml')
gpt_cfg = load_config('configs/part2_gpt_pretrain.yaml')
mt_cfg = load_config('configs/part2_warmstart_mt_v2.yaml')
device = 'cuda' if torch.cuda.is_available() else 'cpu'
encoder = build_bert_encoder(bert_cfg.get('model'), vocab_size=tok.vocab_size)
decoder = build_gpt_decoder(gpt_cfg.get('model'), vocab_size=tok.vocab_size)
model = WarmStartedEncoderDecoder(encoder, decoder, mt_cfg.get('model'))
ckpt = torch.load('results/checkpoints/P2-MT-v2-bi_68534c48046d_best.pt',
map_location=device, weights_only=False)
model.load_state_dict(ckpt['model']); model.to(device).eval()
src = 'मैं घर जा रहा हूँ।'
src_ids = torch.tensor([tok.encode(src, add_bos=True, add_eos=True)], device=device)
with torch.no_grad():
out = model.generate_greedy(src_ids, max_len=128, bos_id=tok.bos_id, eos_id=tok.eos_id)
print(tok.decode(out[0].tolist()))
"
# 6. (Optional) Reproduce evaluation on the test set
python scripts/evaluate_part2.py \
--mt-config configs/part2_warmstart_mt_v2.yaml \
--mt-checkpoint results/checkpoints/P2-MT-v2-bi_68534c48046d_best.pt \
--split test --strategy greedy \
--out-json results/metrics/repro_test_greedy.json
# Expect: BLEU 9.86 / CHRF++ 32.74 over 10 246 test pairs
Phase 1 — Data prep
Downloads the Google-Drive corpus, runs inspection / cleaning / split / SP-BPE tokenizer. Idempotent: re-runs skip completed steps.
modal run modal_app.py::prep_data
modal run modal_app.py::list_volume # diagnostic — confirms data/ + tokenizer/
Output on /vol: data/raw/, data/processed/, data/splits/{train,val,test}.{hi,mr} (228 264 / 5 852 / 10 246 pairs, deterministic seed=42), data/tokenizer/spm_16k_shared.{model,vocab}.
Phase 2 — )
# P1-A — random embeddings
modal run modal_app.py::smoke_part1 --config part1_random.yaml # T4 gate, ~$0.005
modal run --detach modal_app.py::train_part1 --config part1_random.yaml # A10G, ~2.4 h, ~$2.68
# P1-B — L3Cube-BERT frozen embeddings (425M model — A10G needs the bf16-tied-weight fix)
modal run modal_app.py::smoke_part1 --config part1_bert.yaml
modal run --detach modal_app.py::train_part1 --config part1_bert.yaml # A10G, ~4.7 h, ~$5.45
# Evaluation (T4)
modal run modal_app.py::evaluate_part1 --config part1_bert.yaml \
--checkpoint-name P1-B_cec203cdcc90_best.pt --split test --strategy greedy
modal run modal_app.py::evaluate_part1 --config part1_bert.yaml \
--checkpoint-name P1-B_cec203cdcc90_best.pt --split test --strategy beam
# Qualitative (assignment §3.3): 20-example MD tables
modal run modal_app.py::qualitative_part1 --config part1_bert.yaml
modal run modal_app.py::side_by_side_qualitative
# Local CPU — low-frequency word analysis (free)
python scripts/low_freq_analysis.py
Phase 3 — Part II pretraining
# BERT (MLM, ~3.5 h)
modal run modal_app.py::smoke_pretrain_bert
modal run --detach modal_app.py::pretrain_bert
# GPT (causal LM, ~3.7 h)
modal run modal_app.py::smoke_pretrain_gpt
modal run --detach modal_app.py::pretrain_gpt
Phase 4 — Part II MT
The one-command orchestrator runs smoke → Run A → conditional Run B → eval × 4 → final HF push:
modal run --detach modal_app.py::pipeline_warmstart_mt_v2 \
--bert-ckpt results/checkpoints/P2-BERT_c5d7a375e5f5_best.pt \
--gpt-ckpt results/checkpoints/P2-GPT_7ad68b7b3321_best.pt \
--hf-repo Omarrran/IITD_MISN_Hiring_Assignment_Haq_Nawaz \
--hf-token <HF_WRITE_TOKEN> \
--mt-ckpt results/checkpoints/P2-MT-stage1_6a531add3b04_best.pt # optional: warm-start cross-attn from V1
Decision rule for Run B: skip if Run A BLEU < 5 (broken model) or ≥ 15 (diminishing returns); otherwise run B warm-started from A's _best.pt. Run B uses the bidirectional dataset wrapper (src/data/bidirectional.py) with Devanagari language-prefix in the source.
Manual mode (one stage at a time, for inspection):
modal run modal_app.py::smoke_warmstart_mt_v2 --bert-ckpt ... --gpt-ckpt ...
modal run --detach modal_app.py::train_warmstart_mt_v2 --bert-ckpt ... --gpt-ckpt ... # Run A
modal run --detach modal_app.py::train_warmstart_mt_v2 --bert-ckpt ... --gpt-ckpt ... \
--mt-ckpt results/checkpoints/P2-MT-v2-uni_68534c48046d_best.pt --bidirectional # Run B
modal run modal_app.py::evaluate_part2_v2 \
--mt-checkpoint-name P2-MT-v2-bi_68534c48046d_best.pt \
--split test --strategy greedy
modal run modal_app.py::evaluate_part2_v2 \
--mt-checkpoint-name P2-MT-v2-bi_68534c48046d_best.pt \
--split test --strategy beam --alpha-sweep 0.4,0.6,0.8,1.0 --no-repeat-ngram-size 3
modal run modal_app.py::qualitative_part2 \
--mt-checkpoint-name P2-MT-v2-bi_68534c48046d_best.pt
Phase 5 — Plots, train-side metrics, HF release
# Train-side BLEU/CHRF at 7 checkpoints per run (assignment §2 requires train+val plots)
modal run modal_app.py::compute_train_metrics_part1 --config part1_random.yaml --exp-id P1-A --use-best-pt
modal run modal_app.py::compute_train_metrics_part2 --exp-id P2-MT-v2-uni \
--steps 500,2000,5000,8000,11000,13750,15000
modal run modal_app.py::compute_train_metrics_part2 --exp-id P2-MT-v2-bi \
--steps 250,1500,5000,9000,12250,13500
# Local plotting (reads TSV + train-metrics JSON, writes PNGs)
python scripts/plot_part1.py
python scripts/plot_part2_pretraining.py
python scripts/plot_part2_mt.py
Repository Layout — File by File
src/ Library
| Path | What it does |
|---|---|
src/utils/seed.py |
Deterministic seeding for python/numpy/torch/cuda |
src/utils/logger.py |
File + console logger setup |
src/utils/config.py |
YAML config loader with content-addressed hash (load once, hash once, propagate everywhere) |
src/utils/plotting.py |
plot_all(tsv, out_dir, run_id) — produces train + val loss/BLEU/CHRF PNGs from TSV alone |
src/data/download.py |
Pulls corpus from the assignment Google Drive folder, writes SHA-256 manifest |
src/data/inspect.py |
Generates results/data_report.md + 6 inspection plots + stats JSON |
src/data/clean.py |
NFC + whitespace normalize + length/ratio/purity filters + pair-level dedup with per-filter audit |
src/data/split.py |
Deterministic 95/2.5/2.5 carve (seed 42), SHA-256 manifest of every split |
src/data/tokenizer.py |
SentencePiece BPE 16k shared (Hi + Mr), SPTokenizer + BertWPWrapper |
src/data/dataset.py |
ParallelDataset (hi2mr/mr2hi), MLMDataset, CausalLMDataset, parallel_collate |
src/data/mlm_masking.py |
Whole-word MLM collate (Devanagari-aware), 80/10/10 substitution, specials never masked |
src/data/bidirectional.py |
V2 Run B dataset wrapper: alternates hi→mr and mr→hi with Devanagari language-prefix tokens |
src/models/part1/embeddings.py |
Random Xavier / L3Cube-BERT extraction with freeze toggle |
src/models/part1/encoder.py |
BiLSTM with packed sequences, state bridge to decoder |
src/models/part1/attention.py |
Bahdanau additive attention with bf16-safe mask handling |
src/models/part1/decoder.py |
LSTM-2L + input feeding + Luong combine + optional tied I/O |
src/models/part1/seq2seq.py |
Composer with teacher forcing / scheduled sampling + generate_greedy |
src/models/part2/rmsnorm.py |
bf16-safe RMSNorm (fp32 internal, cast back at end) |
src/models/part2/rope.py |
RoPECache + apply_rope; rotate-half implementation |
src/models/part2/gqa.py |
GQA with config validation; reduces to MHA at group_size=1 |
src/models/part2/block.py |
Pre-norm Transformer block + FFN + causal-mask helper |
src/models/part2/bert_encoder.py |
106.75 M BertLikeEncoder + tied MLM head |
src/models/part2/gpt_decoder.py |
122.49 M GPTDecoder with causal masking + generate() |
src/models/part2/warm_started_ed.py |
CrossAttention (no RoPE) + WarmStartedEncoderDecoder; freeze_for_stage1 / unfreeze_all; encode / decode separation so beam search amortises the encoder |
src/training/losses.py |
LabelSmoothedCrossEntropy with ignore_index (CUDA-gather-safe: clamps ignored positions before gather) |
src/training/schedulers.py |
build_scheduler: ReduceLROnPlateau (Part I) or warmup-cosine (Part II) |
src/training/teacher_forcing.py |
Linear epoch-decay schedule for scheduled sampling |
src/training/checkpoint.py |
Atomic save, _latest.pt pointer, full bundle resume (model + optim + scheduler + best metric) |
src/training/factory.py |
YAML model dict → Seq2SeqAttn for random or L3Cube-BERT init |
src/training/ema.py |
timm-style EMA: shadow weights, apply_to / restore semantics |
src/training/trainer.py |
Generic loop: AMP, grad-accum, clip, eval, log/save, resume, early stop, abort-on-NaN, abort-on-low-BLEU |
src/training/run_part1.py |
Part-I CLI entry, supports --dry-run, idempotent resume |
src/training/run_pretrain_bert.py |
BERT MLM pretraining entry |
src/training/run_pretrain_gpt.py |
GPT causal-LM pretraining entry |
src/training/run_warmstart_mt.py |
V1 two-stage warm-started MT (preserved for record) |
src/training/run_warmstart_mt_v2.py |
V2 single-stage full-finetune + EMA + bidirectional toggle + abort safeguard |
src/eval/metrics.py |
sacrebleu BLEU-100 + CHRF++-100 with tokenize=intl, corpus + sentence variants |
src/eval/greedy.py |
Corpus greedy-decode runner |
src/eval/beam_search.py |
Length-normalised (Wu et al. α = 0.6) beam, beam-1 == greedy verified |
src/eval/beam_search_part2.py |
Part-II analogue using encode / decode API; supports no_repeat_ngram_size |
src/eval/qualitative.py |
20-example table by length bucket, sentence sBLEU / sCHRF |
scripts/ (Called from modal_app.py and locally)
| Script | Purpose |
|---|---|
scripts/qualitative_part1.py |
Part-I greedy qualitative MD (called from Modal qualitative_part1) |
scripts/qualitative_part2.py |
Part-II warm-started MD |
scripts/side_by_side_qualitative.py |
Loads both P1-A and P1-B, decodes the same pool, writes side-by-side MD |
scripts/evaluate_part1.py |
Part-I corpus eval (greedy or beam, val or test) → JSON + raw hyps |
scripts/evaluate_part2.py |
Part-II warm-started corpus eval with ensemble + α-sweep + no_repeat_ngram_size |
scripts/low_freq_analysis.py |
Local: bucket test set by source-token IDF, report per-bucket sBLEU / sCHRF |
scripts/extract_qualitative_rows.py |
Extract 4 rows from qualitative MDs for the LaTeX appendix |
scripts/compute_train_metrics_part1.py |
Decode fixed 500-pair train subset at each checkpoint → JSON (drives train BLEU / CHRF plot) |
scripts/compute_train_metrics_part2.py |
Same for Part II warm-started runs |
scripts/plot_part1.py |
Train + val overlay plots for Part I |
scripts/plot_part2_pretraining.py |
6 individual + 1 overlay for BERT/GPT pretraining trajectories |
scripts/plot_part2_mt.py |
Train + val BLEU/CHRF/loss/LR plots for Run A and Run B + overlay |
scripts/hf_push_project.py |
Mirrors local repo tree to a private HF Hub repo (uses HfApi.upload_folder with ignore_patterns) |
scripts/hf_push_full_release.py |
Staging-based release packager (symlinks large files, see DECISIONS §10.14) |
scripts/modal_pull_bypass.py |
Workaround for the Modal CLI stall on > 830 MB files (used pre-HF-bridge) |
configs/ (All YAML, all content-addressed)
| Config | Hash | Used for |
|---|---|---|
configs/data.yaml |
4ef20738aa95 |
Data pipeline (loaded by every script that reads splits/tokenizer) |
configs/part1_random.yaml |
ae234580eb32 |
P1-A — LSTM-Seq2Seq with random embeddings |
configs/part1_bert.yaml |
cec203cdcc90 |
P1-B — LSTM-Seq2Seq with L3Cube-BERT frozen embeddings |
configs/part2_bert_pretrain.yaml |
c5d7a375e5f5 |
P2-BERT MLM pretraining (~110 M) |
configs/part2_gpt_pretrain.yaml |
7ad68b7b3321 |
P2-GPT causal-LM pretraining (~124 M) |
configs/part2_warmstart_mt.yaml |
6a531add3b04 |
V1 two-stage warm-start (preserved for record; produced the documented plateau) |
configs/part2_warmstart_mt_v2.yaml |
68534c48046d |
V2 single-stage full-finetune (the winning recipe, drives Run A and Run B) |
Do not edit any config YAML mid-experiment.
Config hashes drive checkpoint filenames andTrainer.maybe_resume(); a one-byte change silently restarts from step 0.
tests/ (103 tests, all green)
| File | Coverage |
|---|---|
tests/test_scaffold.py |
9 tests — seed, config-hash, logger |
tests/test_data.py |
12 — NFC, devanagari purity, cleaning pipeline, dataset symmetry, collate masking |
tests/test_part1.py |
12 — encoder pad-no-leak, attention masking, tied I/O, overfit-on-4-sentences |
tests/test_training.py |
14 — losses, schedulers, checkpoint roundtrip, beam-1 == greedy |
tests/test_part2_components.py |
23 — RoPE relative invariance, GQA equivalence, causal-no-future-leak, pad-no-leak |
tests/test_part2_models.py |
21 — parameter counts vs assignment targets, cross-attn pad mask, stage1 trainable fraction |
tests/test_mlm_masking.py |
8 — word-boundary lookup, specials never masked, whole-word atomicity |
tests/test_ema_and_bidirectional.py |
4 — EMA decay, bidirectional dataset prefix correctness |
Run All Tests
python -m pytest tests/ -q
Expected output:
103 passed, ~30 s on CPU
Compute budget — actual ledger
Modal billing across two profiles (the first profile depleted mid-GPT-pretrain; we migrated state via the HF Hub bridge — see MIGRATION.md and report/report_part_II_pretraining.md §7).
| Phase | GPU | Wallclock | Cost |
|---|---|---|---|
| Prep data, smokes, image build | CPU + T4 | ~10 min | ~$0.15 |
| P1-A full | A10G | 2.43 h | $2.68 |
| P1-B OOM cascade (5 failed launches) | A10G | ~5 min cum | $0.04 |
| P1-B full | A10G | 4.74 h | $5.45 |
| P1-B qualitative + side-by-side + test greedy + test beam | T4 | ~35 min | $0.34 |
| P2-BERT pretrain | A100-40GB | 3 h 27 min | $6.87 |
| P2-GPT pretrain (pre-migration, step 0 → 14 000) | A100-40GB | ~2 h 30 min | $7.70 |
| HF upload + download (cross-profile migration) | CPU | ~6 min | $0.22 |
| P2-GPT pretrain (post-migration, step 14 000 → 20 000) | A100-40GB | 1 h 13 min | $2.75 |
| V1 stage 1 (plateaued, preserved) | A10G | ~2 h | $2.30 |
| V2 Run A | A10G | 5 h 45 min | $6.30 |
| V2 Run B | A10G | ~6 h (timeout) | $6.30 |
| Part II eval (val + test, greedy + beam, ensemble + α-sweep) | T4 | ~1 h | $0.60 |
| Train-metrics computation × 4 runs | T4 | ~30 min | $0.30 |
| HF full releases × several | CPU | ~15 min cum | $0.40 |
| Total | ~$42 |
The original $30 budget was held within the first profile through the end of P2-GPT pretraining; everything from Run A onwards was paid out of the second $30 profile (and not all of it was used).
Environment
- Python 3.12.10 (Windows local). Modal containers also use 3.12.
- Torch 2.5.1 CUDA-12.4 on Modal GPU image; 2.12.0 CPU on local for data prep and tests.
- transformers 4.46.3, tokenizers 0.20.3, sentencepiece 0.2.0, sacrebleu 2.4.3.
- Modal 0.66.46 (authenticated via
modal token new). - CUDA 12.4 on Modal (
nvidia/cuda:12.4.1-base-ubuntu22.04). - Mixed precision: bf16 throughout (Part II requires Ampere or newer; A10G and A100 both qualify).
- Hugging Face Hub bridge requires huggingface_hub 0.26.2 (auto-installed into a dedicated CPU image inside
modal_app.py).
Known limitations / honest caveats
- Single-seed runs (seed=42). BLEU variance on a 500-sentence eval subset is typically ±0.3–0.5; absolute numbers should be read with one-significant-figure precision. The Run A → Run B improvement (+1.84 BLEU) is well outside this band.
- Run B incomplete at step 13 600 / 15 000. Modal's per-function 6-hour timeout terminated training before cosine end. The remaining ~1 400 steps would have been at LR ≤ 3 % of peak; BLEU had already plateaued for ~1 350 steps.
- Run A test-set numbers (beam) are pending in some JSONs — Modal incident on 2026-05-19 delayed those launches; the
results/metrics/directory captures whatever did complete. - mr2hi reported via bidirectional Run B only — deliberate budget choice. The §4.1 "Hi ↔ Mr" requirement is interpreted as framework-supports-both, with hi→mr as the canonical evaluation direction.
modal volume getstalls/corrupts files > 830 MB on the author's Windows setup — bypassed via the HF Hub bridge (hf_upload_checkpoints/hf_download_checkpoints).- No back-translation / no external monolingual data — assignment forbids external parallel data, and we did not augment with monolingual back-translation either. This is the standard +2–5 BLEU low-resource trick (Sennrich et al. 2016) and is left for future work within the data constraints.
Hashes of every config and every produced checkpoint are content-addressed ` and at the top of each report. Reproducing a run from a YAML at the listed hash will produce an identical checkpoint filename and (modulo CUDA non-determinism in some kernels) numerically near-identical training trajectories.