Skip to content

Commit 0665270

Browse files
author
Ronald Tse
committed
feat: CPU training path, GH release workflow, Modal GPU, npm manifest
Adds the missing pieces between framework skeleton and first real release. End-to-end CPU training is now demonstrable. CPU training: - framework/device.py: device resolver (auto-detects cuda/mps/cpu, honors INTERSCRIPT_DEVICE env var) - framework/trainer.py: StudentTrainer trains student directly on gold labels without a teacher; works on CPU - ModelConfig.device + TrainConfig.max_steps_per_epoch fields - pipeline defaults to StudentTrainer (CPU-friendly); DistillTrainer remains available for production runs Release infrastructure: - .github/workflows/release.yml: tag-driven workflow that builds fp32 + q8 ONNX, computes SHA256, generates release notes, creates GH release, attests SLSA provenance, syncs to HF (gated on secret) - scripts/generate_release_notes.py emits Markdown body for the GH Release from benchmark JSON - src/cli.py: --variant flag on export subcommand Cloud GPU: - src/gpu/modal_train.py: Modal integration, one-command A10G/A100 training with per-second billing, ~$20/task - docs/gpu-options.md: decision matrix (Modal vs Lambda Labs vs Colab vs sponsored credits), cost estimates per task npm manifest package: - npm/models/: @interscript/models placeholder. Tiny JSON that maps task to version + URL + checksum. Consumed by interscript-ts to resolve the "default" model version - scripts/update_npm_manifest.sh: post-release script that pulls asset checksums from GH API and bumps the manifest E2E proof: - tests/test_e2e_cpu.py: full train to export to onnxruntime inference loop, skipping gracefully when torch is not installed - 39 tests now pass; ruff clean - Manually verified: CLI train + export produces a 28KB ONNX file that loads + runs in onnxruntime
1 parent 8575a3b commit 0665270

16 files changed

Lines changed: 1002 additions & 12 deletions

.github/workflows/release.yml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: release
2+
3+
# Triggered by per-task tags: rababa_arabic-v1.0.0, secryst_thai_ipa-v0.1.0, etc.
4+
# Spec: TODO.distribution/01-github-releases.md
5+
on:
6+
push:
7+
tags: ["*-v*.*.*"]
8+
9+
permissions:
10+
contents: write
11+
attestations: write
12+
id-token: write
13+
14+
jobs:
15+
parse-tag:
16+
runs-on: ubuntu-latest
17+
outputs:
18+
task: ${{ steps.parse.outputs.task }}
19+
version: ${{ steps.parse.outputs.version }}
20+
is_prerelease: ${{ steps.parse.outputs.is_prerelease }}
21+
steps:
22+
- id: parse
23+
run: |
24+
tag="${GITHUB_REF_NAME}"
25+
task="${tag%-v*}"
26+
version="${tag#*-v}"
27+
prerelease=false
28+
if [[ "$version" == *"-alpha."* || "$version" == *"-beta."* || "$version" == *"-rc."* ]]; then
29+
prerelease=true
30+
fi
31+
echo "task=$task" >> $GITHUB_OUTPUT
32+
echo "version=$version" >> $GITHUB_OUTPUT
33+
echo "is_prerelease=$prerelease" >> $GITHUB_OUTPUT
34+
35+
build:
36+
needs: parse-tag
37+
runs-on: ${{ matrix.runner }}
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
include:
42+
- variant: fp32
43+
runner: ubuntu-latest
44+
extras: "train,export"
45+
- variant: q8
46+
runner: ubuntu-latest
47+
extras: "export"
48+
steps:
49+
- uses: actions/checkout@v4
50+
- uses: actions/setup-python@v5
51+
with: { python-version: "3.11" }
52+
- name: Install deps
53+
run: |
54+
python -m pip install --upgrade pip
55+
pip install -e ".[${{ matrix.extras }}]"
56+
- name: Build ONNX (${{ matrix.variant }})
57+
env:
58+
TASK: ${{ needs.parse-tag.outputs.task }}
59+
VARIANT: ${{ matrix.variant }}
60+
run: |
61+
mkdir -p models/$TASK
62+
if [ "$VARIANT" = "fp32" ]; then
63+
python -m src.cli export --task $TASK --data-root data --out-root models/$TASK
64+
else
65+
python -m src.cli export --task $TASK --variant $VARIANT --data-root data --out-root models/$TASK
66+
fi
67+
- name: Compute SHA256
68+
env:
69+
TASK: ${{ needs.parse-tag.outputs.task }}
70+
VARIANT: ${{ matrix.variant }}
71+
run: |
72+
cd models/$TASK
73+
for f in *.onnx; do
74+
sha256sum "$f" > "$f.sha256"
75+
done
76+
- uses: actions/upload-artifact@v4
77+
with:
78+
name: onnx-${{ matrix.variant }}
79+
path: models/${{ needs.parse-tag.outputs.task }}/
80+
81+
benchmark:
82+
needs: [parse-tag, build]
83+
runs-on: ubuntu-latest
84+
steps:
85+
- uses: actions/checkout@v4
86+
- uses: actions/setup-python@v5
87+
with: { python-version: "3.11" }
88+
- run: pip install -e ".[dev,export]"
89+
- uses: actions/download-artifact@v4
90+
with:
91+
name: onnx-fp32
92+
path: models/${{ needs.parse-tag.outputs.task }}
93+
- name: Evaluate
94+
env:
95+
TASK: ${{ needs.parse-tag.outputs.task }}
96+
run: |
97+
python -m src.cli evaluate --task $TASK --data-root data --out-root models \
98+
> $TASK-benchmarks.json
99+
- uses: actions/upload-artifact@v4
100+
with:
101+
name: benchmarks
102+
path: ${{ needs.parse-tag.outputs.task }}-benchmarks.json
103+
104+
release:
105+
needs: [parse-tag, build, benchmark]
106+
runs-on: ubuntu-latest
107+
steps:
108+
- uses: actions/checkout@v4
109+
- uses: actions/download-artifact@v4
110+
with: { path: artifacts }
111+
- name: Stage release assets
112+
env:
113+
TASK: ${{ needs.parse-tag.outputs.task }}
114+
run: |
115+
mkdir -p release/$TASK
116+
cp artifacts/onnx-fp32/* release/$TASK/ 2>/dev/null || true
117+
cp artifacts/onnx-q8/* release/$TASK/ 2>/dev/null || true
118+
cp artifacts/benchmarks/* release/$TASK/ 2>/dev/null || true
119+
ls -lhR release/
120+
- name: Generate release notes
121+
env:
122+
TASK: ${{ needs.parse-tag.outputs.task }}
123+
VERSION: ${{ needs.parse-tag.outputs.version }}
124+
run: python scripts/generate_release_notes.py --task $TASK --version $VERSION
125+
- name: Create GitHub Release
126+
uses: softprops/action-gh-release@v2
127+
with:
128+
prerelease: ${{ needs.parse-tag.outputs.is_prerelease }}
129+
body_path: RELEASE_NOTES.md
130+
files: |
131+
release/**/*
132+
- name: Attest build provenance
133+
uses: actions/attest-build-provenance@v1
134+
continue-on-error: true
135+
with:
136+
subject-name: github.com/interscript/ml-models
137+
subject-digest: sha256:${{ github.sha }}
138+
139+
publish-hf:
140+
needs: release
141+
runs-on: ubuntu-latest
142+
if: false # enable once HF_TOKEN secret is set
143+
steps:
144+
- uses: actions/checkout@v4
145+
- uses: actions/setup-python@v5
146+
with: { python-version: "3.11" }
147+
- run: pip install -e ".[publish]"
148+
- uses: actions/download-artifact@v4
149+
with: { path: artifacts }
150+
- env:
151+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
152+
TASK: ${{ needs.parse-tag.outputs.task }}
153+
run: |
154+
python -m src.cli publish --task $TASK --repo interscript/$TASK

docs/gpu-options.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# GPU options — where to train
2+
3+
We need a GPU for the heavy lifting (teacher fine-tune + distillation).
4+
The framework's CPU path works for dev / CI / mobile variants, but
5+
production training needs CUDA. Here are the realistic options.
6+
7+
## Cost vs need
8+
9+
| Task | Wall time on 1× A100 80GB | Est. cost (Modal A100) |
10+
|---|---|---|
11+
| rababa_arabic teacher fine-tune (Qwen3.5-4B LoRA) | 12-18h | ~$15-25 |
12+
| rababa_arabic student distillation | 3-4h | ~$5-7 |
13+
| rababa_arabic ONNX export + benchmarks (CPU) | 5min | $0 |
14+
| **rababa_arabic total** | **~16-22h** | **~$20-30** |
15+
| rababa_hebrew (same shape) | ~16-22h | ~$20-30 |
16+
| secryst_thai_ipa (same shape) | ~16-22h | ~$20-30 |
17+
| **All three tasks** | **~50-66h** | **~$60-90** |
18+
19+
For comparison: $100 of Modal credits is enough to retrain every task
20+
3-4 times. **Cost is not the blocker.**
21+
22+
## Recommendation
23+
24+
### Tier 1: Modal (default for dev) — serverless A10G/A100
25+
26+
```bash
27+
pip install modal
28+
modal token new
29+
modal run src/gpu/modal_train.py --task rababa_arabic
30+
```
31+
32+
**Why:**
33+
- Per-second billing (no minimum)
34+
- No queue, no commitment
35+
- Image is baked; cold start ~30s
36+
- Mounts local code → fast iteration
37+
- A10G ($1.09/hr) is enough for our sizes
38+
- A100 ($3.40/hr) only needed for the 4B teacher
39+
40+
**Code:** `src/gpu/modal_train.py` — already written.
41+
42+
### Tier 2: Lambda Labs (sustained) — A100 80GB at $1.10/hr
43+
44+
Use when retraining all three tasks back-to-back. 50h × $1.10 = $55.
45+
Same hardware as AWS/GCP, half the price, simpler billing. Trade-off:
46+
queues can be hours; reserved instances take a week to provision.
47+
48+
### Tier 3: Colab Free (zero budget) — T4 16GB
49+
50+
T4 fits the student distillation (6M params). Teacher (4B + LoRA) is
51+
tight — needs gradient checkpointing + 4-bit base model loading.
52+
53+
**Workflow:**
54+
1. Open `notebooks/colab_train.ipynb` in Colab Free
55+
2. Runtime → Change runtime type → T4 GPU
56+
3. Run all cells
57+
4. Model auto-uploads to HF Hub at end
58+
59+
Limitations:
60+
- 12h session limit (rababa_arabic teacher fits in 12h)
61+
- T4 is older architecture — 2x slower than A10G
62+
- Storage is ephemeral — must checkpoint to Drive or HF
63+
64+
### Tier 4: HuggingFace Spaces A10G Small (free)
65+
66+
For inference + small jobs only. Not suitable for training (1 GPU,
67+
shared, preemptible). Use for the demo deployment instead.
68+
69+
### Tier 5: AWS / GCP / Azure (sponsored)
70+
71+
Apply for OSS credits. Interscript qualifies for:
72+
- **AWS Open Source Software Sponsorship** ($1-5k credits)
73+
- **GCP for Open Source** ($5-25k credits via Google for Startups OSS)
74+
- **HuggingFace Community Grants** (free compute for OSS ML)
75+
- **NumFOCUS Small Grants** ($3-5k for fiscal-sponsored projects)
76+
- **MLCommons Research Credits** (academic partnerships)
77+
78+
A single AWS sponsorship would cover all training for 12+ months at
79+
our scale. Apply early — these take weeks.
80+
81+
## Decision matrix
82+
83+
| Use case | Recommended |
84+
|---|---|
85+
| First production run | Modal A100 ($20-30/task) |
86+
| Dev iteration | Local CPU (this repo) or Colab T4 |
87+
| Bulk retrain all tasks | Lambda Labs A100 |
88+
| Cheap / free demo | Colab Free T4 |
89+
| Long-term | AWS/GCP OSS credits |
90+
| Mobile/edge variants | Local CPU (no GPU needed) |
91+
92+
## Anti-choices
93+
94+
- **Kaggle Kernels**: 30h/week, but kernel restarts lose progress;
95+
better for one-off demos than sustained work.
96+
- **Vast.ai**: cheapest but reliability issues; intermittent driver
97+
crashes on consumer cards.
98+
- **RunPod serverless**: similar to Modal but smaller community; only
99+
use if Modal raises prices.
100+
- **Local GPU**: only if you already have an A6000 or better. RTX 3090
101+
is borderline — fits student but not teacher.
102+
103+
## Monitoring cost
104+
105+
Modal dashboard shows live cost per run. Set a hard ceiling via:
106+
107+
```python
108+
@stub.function(gpu="A100", timeout=6 * 3600, cpu=8, memory=32 * 1024)
109+
def train_task(task: str):
110+
# Modal aborts if wall time exceeds timeout — no runaway bill.
111+
...
112+
```
113+
114+
Plus: set `INTERSCRIPT_MAX_USD_PER_RUN` env var (TODO in the framework)
115+
to abort if cumulative cost crosses a threshold.

npm/models/manifest.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"schema_version": 1,
3+
"schema_description": "Interscript ML model manifest. interscript-ts + interscript-ruby read this to resolve 'default' model versions. See TODO.distribution/04-npm-packages.md.",
4+
"updated_at": "2026-08-01T00:00:00Z",
5+
"models": {
6+
"rababa_arabic": {
7+
"status": "preview",
8+
"version": "0.0.0",
9+
"note": "No trained release yet. Framework only.",
10+
"cdn_base": "https://cdn.jsdelivr.net/gh/interscript/ml-models@rababa_arabic-v{version}/",
11+
"github_base": "https://github.com/interscript/ml-models/releases/download/rababa_arabic-v{version}/"
12+
},
13+
"rababa_hebrew": {
14+
"status": "preview",
15+
"version": "0.0.0",
16+
"note": "No trained release yet. Framework only.",
17+
"cdn_base": "https://cdn.jsdelivr.net/gh/interscript/ml-models@rababa_hebrew-v{version}/",
18+
"github_base": "https://github.com/interscript/ml-models/releases/download/rababa_hebrew-v{version}/"
19+
},
20+
"secryst_thai_ipa": {
21+
"status": "preview",
22+
"version": "0.0.0",
23+
"note": "No trained release yet. Framework only.",
24+
"cdn_base": "https://cdn.jsdelivr.net/gh/interscript/ml-models@secryst_thai_ipa-v{version}/",
25+
"github_base": "https://github.com/interscript/ml-models/releases/download/secryst_thai_ipa-v{version}/"
26+
}
27+
},
28+
"conventions": {
29+
"tag_format": "<task>-v<MAJOR>.<MINOR>.<PATCH>",
30+
"asset_naming": "<task>-<variant>.onnx where variant is one of: (none=fp32) | q8 | q4 | fp16",
31+
"checksum_sidecar": "<asset>.sha256 (lowercase hex, single space, filename)",
32+
"fallback_chain": ["cdn_base", "github_base", "huggingface.co/interscript/<task>/resolve/main/"]
33+
}
34+
}

npm/models/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@interscript/models",
3+
"version": "0.0.1",
4+
"description": "Manifest of available Interscript ML models + their current versions, URLs, and checksums.",
5+
"license": "MIT",
6+
"main": "manifest.json",
7+
"files": [
8+
"manifest.json"
9+
],
10+
"keywords": [
11+
"interscript",
12+
"ml",
13+
"models",
14+
"manifest",
15+
"transliteration",
16+
"rababa",
17+
"secryst"
18+
],
19+
"homepage": "https://github.com/interscript/ml-models#readme",
20+
"repository": {
21+
"type": "git",
22+
"url": "git+https://github.com/interscript/ml-models.git"
23+
},
24+
"author": "Interscript Project"
25+
}

0 commit comments

Comments
 (0)