← TinyGPT · docs · devlog · roadmap · speedup
source: docs/mac_app_plan.md · view on GitHub ↗

Native macOS app — build plan

Historical app plan. The current Mac app stance is parked except for a future minimal Factory Run Center after the CLI factory loop proves improvement. See docs/parked/app-polish.md and docs/factory/overview.md.

The browser playground is the on-ramp. The Mac app is the depth: same architecture, same .tinygpt file format, ~20-30× the training throughput. This doc translates roadmap lever 20 into a concrete week-by-week build plan you can act on.

Boundary is already drawn in docs/archive/shared_vs_native.md — read that first. This doc picks up where it leaves off: what to actually build, in what order, and what each milestone proves.

Why MLX-Swift, not Python MLX

MLX (Python) is faster to prototype but ships a Python interpreter and a ~150 MB MLX dylib stack with every app. MLX-Swift compiles into the app binary, links MLX statically through SwiftPM, and produces a single notarizable .app under ~50 MB. The Swift API is a thin wrapper over the same C++ MLX core — same kernels, same perf, no interpreter tax. The distribution + UX wins are decisive for a public app.

MVP scope (what ships in v0.1)

The MVP is “the browser playground, but native.” Same UI ideas, same model, same file format — Metal where WebGPU was.

In:

Out (deferred to v0.2+):

Out forever:

File / module layout

mac/
├── Package.swift                    # SwiftPM, depends on MLX-Swift
├── Sources/
│   ├── TinyGPTApp/                  # SwiftUI entry point
│   │   ├── TinyGPTApp.swift         # @main, scene
│   │   ├── ContentView.swift        # the two-screen split
│   │   ├── SetupView.swift          # preset picker + start
│   │   ├── WatchView.swift          # loss chart + samples
│   │   └── GalleryView.swift        # pre-trained model loader
│   ├── TinyGPTModel/                # the model — pure MLX-Swift
│   │   ├── Model.swift              # Transformer, parity with python_ref/model.py
│   │   ├── Attention.swift          # multi-head + RoPE
│   │   ├── MLP.swift                # SwiGLU MLP block
│   │   ├── Tokenizer.swift          # byte-level + small BPE
│   │   ├── Optimizer.swift          # AdamW
│   │   └── TrainStep.swift          # forward + backward + AdamW step
│   ├── TinyGPTIO/                   # the file format
│   │   ├── TinyGPTFile.swift        # .tinygpt reader/writer
│   │   ├── Manifest.swift           # JSON header schema
│   │   └── Tensors.swift            # MLXArray ↔ flat-blob conversion
│   └── TinyGPTUI/                   # shared design primitives
│       ├── Theme.swift              # accent / panel / line colors
│       ├── LossChart.swift          # SwiftUI canvas mirror of charts.ts
│       └── Pills.swift              # capability pills (Metal, ANE, etc.)
├── Resources/
│   ├── gallery/                     # bundled .tinygpt files
│   └── AppIcon.appiconset/
├── Tests/
│   ├── ParityTests/                 # browser ↔ Mac round-trip checks
│   │   ├── FileFormatTests.swift    # write → read → bit-identical
│   │   └── NumericsTests.swift      # forward pass matches python_ref
│   └── ModelTests/
│       └── TrainStepTests.swift     # one AdamW step loss-descent
└── README.md

Keep mac/ at the repo root, sibling to browser/. Don’t merge into browser/ — Swift Package Manager wants clean ownership of its directory tree.

Milestone sequence

Each milestone is a few days of work and produces a runnable artifact. Don’t skip ahead; the value of TinyGPT-the-product was always “every milestone is demoable.”

M1 — file format parity (1-2 days)

Goal: a Swift CLI tool that reads a browser-written .tinygpt file and prints the same manifest the browser would print.

What’s in:

What’s out: any actual model — just file format.

Why this first: the file format is the only hard cross-path contract. If it’s broken, every later milestone is built on sand. Verifying it in isolation, before any model code, keeps the contract honest.

M2 — forward-pass numerics parity (3-4 days)

Goal: a Swift CLI that loads a .tinygpt file, runs a forward pass on a single token, and produces logits identical to python_ref/model.py within tight numerics (< 1e-4 max-abs on float32 path).

What’s in:

What’s out: backward pass, training loop, UI.

Why: training is much harder to debug than inference. Get forward right first; the gradient code can then be checked against autograd one layer at a time.

M3 — training loop end-to-end (3-4 days)

Goal: a Swift CLI that takes a corpus + config and trains a Tiny model for 500 steps, producing a .tinygpt checkpoint the browser can load and continue training from.

What’s in:

What’s out: UI, gallery, samples.

Why this is the milestone that unlocks everything else: once training works, the rest is plumbing. This is the highest-risk piece — give it breathing room.

M4 — SwiftUI shell (2-3 days)

Goal: the app opens, the Setup screen shows, you can pick a preset and start training, the Watch screen shows the loss chart updating live.

What’s in:

What’s out: gallery dialog, file open/save UI, LoRA, quantization.

Why: this is the demo. Once it exists, the app is real even if it has zero non-MVP features.

Goal: you can save a trained checkpoint to disk, reopen it, continue training. Gallery dialog with the four bundled corpora.

What’s in:

Why: ship M5 and the MVP is done. Cut a v0.1 DMG.

M6 — packaging + distribution (1-2 days)

Goal: a notarized DMG you can drop on a public landing page next to the browser playground.

What’s in:

Why: this is when “Mac app” stops being a repo subdirectory and starts being a product.

v0.2 features (after MVP ships)

In rough priority:

  1. LoRA fine-tuning (lever 19) — “load a gallery model, paste your own text, watch it adapt.” This is the headline post-MVP feature. MLX-Swift supports LoRA primitively, so most of the work is UI.
  2. Quantized inference (lever 19) — load int4 / int8 checkpoints for sample-only paths. Lets the bundled gallery shrink ~4×.
  3. ANE-accelerated sampling — MLX-Swift exposes a useANE flag on selected ops; profile sampling with and without.
  4. Larger presets — Mega (24L, d=512) and Behemoth-Pro (24L, d=768). Browser can’t run these; the Mac app’s parameter ceiling makes them demoable.

Decisions to make before starting

These are the choices that will be hard to reverse — surface them now so they’re not bikesheds mid-project.

1. SwiftUI minimum macOS version

Lean toward macOS 14 (Sonoma, 2023). Anything later restricts the audience without unlocking much; anything earlier means skipping recent SwiftUI features (@Observable, modern Charts). Picking 14 covers 90%+ of M-series Mac users.

2. Single window vs. document-based

Single window. The browser playground is single-window; matching that keeps the UX patterns parallel and avoids the document-architecture overhead (auto-save, version history, etc.) for a project that has exactly one file type.

3. Hand-tuned Metal vs. pure MLX-Swift

Pure MLX-Swift for v0.1. Custom Metal kernels are a v0.3+ optimization — “we hit the MLX-Swift ceiling, here’s the next 1.3×.” MLX-Swift itself is already ~25× the browser; that’s the win we ship with.

4. Tokenizer parity

Byte-level for MVP (matches browser default). BPE comes later when the Mac app starts training models too big for byte-level to be practical (roughly Mega and above). The tokenizer module is small; switching is not a one-way door.

5. Telemetry

None for v0.1. The browser uses opt-in PostHog; the Mac app’s first release should be telemetry-free to keep the trust bar high. Add later if there’s a concrete question only telemetry can answer.

Open risks

Estimated total

MVP (M1-M6): ~2-3 weeks of focused work. v0.2 (LoRA + quant): another ~1-2 weeks.

Don’t try to ship MVP + v0.2 together. The browser launch taught the lesson: small artifacts ship, big ones rot.