Pick the model first. It picks the training hardware, and it can rule out the managed service.
By Keith Townsend · October 21, 2026
In Lab 26 I fine-tuned Gemma 4 26B, a mixture-of-experts (MoE) model, on an M5 Ultra Mac Studio and an NVIDIA DGX Spark. The Studio was six times faster. In Lab 24, a similar job on CoreWeave Forge took 31 minutes end to end. So do those results carry through to a dense 31B model? That's the least interesting part of the question. The answer drives an architectural buying decision: you have to know what model you need to train before you evaluate training performance. Here the model was Lab 18's coding-agent tune of the dense Gemma 4 31B. Forge's self-serve menu doesn't list it. The Spark trained it only on an older, pinned software release. The Studio trained it 3.8 times faster, after I rewrote part of its software. Both adapters landed in the same range on Lab 18's coding tasks, 11 to 14 of 22.
One dense model (Gemma 4 31B, bf16), one long-sequence LoRA job and Lab 18's 22-task coding loop as the outcome bar, one DGX Spark and one Mac Studio (M5 Ultra, 256 GB), one training run and one loop run per arm. The venues are compared as services at the contract layer: whatever runtime each needed to take the job is part of the venue, and what it took is its cost. The Spark ran Lab 18's own PyTorch stack and vLLM; the Studio ran mlx-lm with attention I rewrote to fit, and mlx-lm's server with a cache cap. Power is each GPU's rail as its system reports it. CoreWeave Forge was checked against its published self-serve list, not run. A faster Spark software stack is the open question.
Do Pick the model your use case needs, then check which venues can train it at all. CoreWeave Forge's published self-serve list for serverless fine-tuning (read October 21, 2026) has five base models, with the LoRA rank fixed per model. The dense Gemma 4 31B isn't on it. I didn't ask CoreWeave's support whether it can be enabled; on the self-serve path, the managed route ended before speed or effort came up.
Do Prove the job fits at its longest example, on the exact stack you'll run, before you compare speed. The Spark trained the 30,553-token example only on Lab 18's pinned transformers release, with as little as 5.7 GB of its 121 GB free; the current release took 2.0 times a 26B model's weights in memory while loading, against 1.74 times for the pinned one. On the Studio neither framework's stock training path fit: mlx-lm ran out of memory at a 224 GB footprint, and PyTorch's Apple GPU backend stopped at its indexing limit, about 11,600 tokens.
Do Count the Mac Studio as a training and serving venue for this model, and budget the engineering to get it there. With rewritten attention it trained the job in 2.69 hours against the Spark's 10.25. On Lab 18's 22-task bar, the adapters from both boxes scored 11 to 14 on either box, inside Lab 18's own noise (its tune 14, its base 12). Serving the loop, the Studio took 1 hour 46 minutes to about 2.5 hours per adapter against the Spark's 5 hours 17 to 5 hours 41.
Don’t Don't treat the OpenAI-compatible API as the portability layer. The loop spoke the same API to both servers. vLLM ran Lab 18's recorded settings unchanged; mlx-lm's server ran out of memory with 51 GB of old conversations in its prompt cache and needed a 32 GB cap. The runtime's operating settings move with the workload: self-serve them and they're yours, take a managed endpoint and they're the provider's.
Don’t Don't buy the Spark expecting its compute to carry this job. In a matrix-multiply benchmark the Studio's GPU ran about 1.2 times the Spark's at bf16, with 4.6 times the memory bandwidth. The 3.8x training gap sits between those, nearer bandwidth. Whether a faster Spark software stack closes it is untested.
Self-funded editorial. The DGX Sparks and the Mac Studio were bought by the practice. Disclosure: Google Cloud is a client of this practice, and the model under test, Gemma 4 31B, is a Google model. Google did not commission, fund, preview or have any say in this lab. Apple and NVIDIA hardware are both under test; CoreWeave's Forge appears through its published model list and Lab 24's record. Neither Apple, NVIDIA, CoreWeave nor Anthropic is a client. No vendor paid for this answer.
In Lab 26 I fine-tuned Gemma 4 26B, a mixture-of-experts (MoE) model, on an M5 Ultra Mac Studio and an NVIDIA DGX Spark. The Studio was six times faster. In Lab 24, a similar job on CoreWeave Forge took 31 minutes end to end. So do those results carry through to a dense 31B model?
That's the least interesting part of the question. The answer drives an architectural buying decision. You have to know what model you need to train before you evaluate training performance.
Which model?
The heaviest fine-tune in this corpus is Lab 18's. It trains the dense Gemma 4 31B on coding-agent transcripts that run to 30,553 tokens, with a LoRA adapter, a small set of extra weights on top of the model. Lab 18 then scored it in a 22-task coding loop, where its tune solved 14 and the base model solved 12. Lab 18 called that gap noise. I took the same job, the same data and the same recipe to three venues.
The managed one went first, and it went fast. Forge's self-serve list for serverless fine-tuning has five base models, with the LoRA rank fixed for each. The dense 31B isn't on it. I didn't ask CoreWeave's support team whether it could be enabled, so this is the self-serve path, not a rejection. But the point holds. A managed service is only as good as its model selection, and here the question of speed never came up.
Did the owned boxes take it?
Yes, but neither took it as shipped.
The Spark trained it only on the exact software Lab 18 ran. Lab 18's launcher installed whatever release of the transformers library was current. Today's release held about two copies of the weights while loading, and on a 121 GB Spark I stopped the 31B load before it could run the box out of memory. Lab 18's own error log named its release, and pinned to it, the longest example trained with 5.7 GB to spare. That's a thin margin.
The Studio had 256 GB and still couldn't run the stock path. Training through mlx-lm, with its stock attention, ran out of memory at 224 GB on the 30,553-token example. PyTorch on the Mac's GPU stopped at its indexing limit, near 11,600 tokens. The culprit is attention, which builds a score matrix that grows with the square of the length. I rewrote it to work a block at a time.
So what does that make the Studio? At this level I'm not comparing technical features. I'm consuming a service at the contract layer: does the venue take the model, train it, and serve it? Which runtime it used is an implementation detail below the value line. What it took to get there is the cost of the venue.
Was it faster, and was it as good?
The Studio trained the job in 2.69 hours against the Spark's 10.25. That's 3.8 times faster, down from six on the smaller job. A matrix-multiply benchmark put the Studio's GPU at about 1.2 times the Spark's, with 4.6 times the memory bandwidth, so the gap sits nearer bandwidth than compute. I haven't tried a faster software stack on the Spark. That's the next test.
Then I scored both adapters in Lab 18's coding loop, on both boxes. Every run landed between 11 and 14 of 22. The Spark-trained adapter solved 11 on the Spark and 14 on the Studio. The Studio-trained one solved 13 and 12. That's the same outcome, inside the noise Lab 18 already measured. The two adapters predicted the same next token 98.3% of the time on held-out examples.
Serving the loop, the Studio was faster again. It ran each adapter's 22 tasks in 1 hour 46 minutes to about 2.5 hours, against more than five on the Spark.
What broke when I moved it?
The serving software did. Both boxes spoke the same OpenAI-compatible API to the coding loop. vLLM on the Spark ran Lab 18's recorded settings unchanged. mlx-lm's server on the Studio kept old conversations in its cache until 51 GB of them ran the box out of memory. It needed a 32 GB cap to finish.
That's a portability finding. I can take a vLLM workflow to any provider running vLLM. Moving it to another runtime behind the same API meant changing settings the API never shows. Serve it yourself and those settings are yours. Take a managed endpoint and they're the provider's.
So what?
Pick the model first. It decides which venues can take the job at all, and what each one costs to get there. On this one, the managed service was out on its own menu. The Spark worked on pinned software with a thin margin. The Studio was faster at training and serving, once I rewrote part of it.
That's not a verdict that the Mac beats the Spark. It's a buying order. Name the model, find where it fits, and only then ask which venue is fastest.
The numbers
Where each layer belongs
Read for portability. Could you take this layer elsewhere without rebuilding? How to read this table
| Layer | Placement |
|---|---|
Layer 0A · Compute Compute Mac Studio training: MLX and mlx-lm with a rewritten attention function in plain MLX. Owned, open-source trainer; the same job and adapters ran on NVIDIA hardware too, so nothing ties the workload to Apple silicon. Follows Labs 21, 24 and 26. | Retained |
Layer 0A · Compute Compute DGX Spark training: PyTorch, transformers 5.15.1 and PEFT in NVIDIA's NGC container. Commodity NVIDIA hardware is Retained by the instrument's rule. | Retained |
Layer 0A · Compute Compute Serving runtime under the OpenAI-compatible API, self-served: vLLM 0.25.1 on the Spark and mlx-lm's server on the Studio. The runtime's operating settings (cache budget, concurrency, memory) are the operator's. Keith's 2026-10-21 rule: the underlay is Retained when self-served and Ceded on a managed route. | Retained |
Layer 0A · Compute Compute A managed fine-tune-and-serve route such as Forge (not run here): the provider sets the model menu, the LoRA rank and placement, and the serving runtime and its settings, behind the same API. Lab 24 measured Forge's own paths. | Ceded |
Layer 2C · Reasoning Agentic Infrastructure — The Reasoning Plane The trained adapters from either box. Open weights, standard PEFT and mlx-lm files, translated by an exact transpose, served on both runtimes with the same outcomes. | Retained |
Sources
Method and disclosure
Self-funded. The DGX Spark and the Mac Studio were bought by the practice. No vendor was briefed, reviewed this, or was given advance sight. Google Cloud is a client of this practice, and the model under test is a Google model; Google had no part in this lab.
Inputs: Lab 18's corpus run once through Lab 18's own build function under its pinned release (171 kept, 9 dropped, ids sha256 21c9ee21720614a2, byte-identical under transformers 5.15.1 and 5.20.2). The 2-epoch order drawn by Lab 18's trainer with its seeded sampler, checked by replaying the trainer. One starting adapter, seed 42, shared by both boxes.
Spark training: Lab 18's train_lora_traj.py patched only to read the shared ids, order and starting adapter, in NVIDIA's NGC PyTorch 25.11 container with transformers 5.15.1, accelerate 1.14.0 and PEFT 0.18.0 baked in. Lab 18's release was identified from its own error traceback, line for line. Gradient checkpointing and a chunked loss that skips Gemma's final-logit softcap (a cap of 30 on the output scores), as Lab 18 ran. A watchdog stopped the container below 3 GB free; it never fired.
Studio training: MLX 0.32.4 and mlx-lm 0.32.2, base converted to MLX bf16 with stock mlx-lm, which drops Gemma 4's vision tower (about 1 GB; text-only training never runs it, and the Spark loads it unused). A trainer written to Lab 18's recipe: the same chunked uncapped loss, per-micro-batch averaging over 4, clipping at 1.0, AdamW with torch's defaults, cosine without warmup, per-layer gradient checkpointing. Attention computed in blocks of 2,048 queries with mx.checkpoint per block: full-attention layers through the fused kernel's causal mode, sliding-window layers through small block masks.
Loop eval: Lab 18's p7 loop (stateful.py, its 22-task list, two turns, thinking on, 49,152-token budget, two tasks in flight), unchanged on the Spark apart from the results folder. Spark serving: Lab 18's tuned-serve container settings, recovered from the stopped container (vLLM 0.25.1, runtime LoRA, 131,072-token context, 0.85 memory utilization), with only the adapter mount changed. Studio serving: stock mlx-lm server bound to localhost with Gemma's sampling defaults, a 32 GB prompt-cache cap, and a ten-line wrapper so the loop's model name resolves to the adapter; the loop and its Docker test sandbox ran on a separate Mac through an SSH tunnel, not on the Studio. The loop's job control was replaced there by a two-worker driver, after macOS's bash ran all 22 tasks at once.
Validation: both adapters and Lab 18's original translated by an exact transpose and scored with Lab 18's loss on the 21 held-out examples, in both runtimes.
Benchmark: bf16 matrix multiply at square and job shapes, a 4 GiB bandwidth test and a 10-minute sustained run, in each box's training stack. MLX's fp32 matmul defaults to TF32-class precision (MLX issue #3860), so exact fp32 references were computed on the Studio's CPU.
Power: the Studio's GPU channel from IOReport, read without sudo; the Spark's nvidia-smi power.draw every 5 seconds.
Forge: CoreWeave's available-models page for serverless training, read October 21, 2026. Nothing was sent to Forge.
Peer check: the draft was attacked by two outside models (ChatGPT and Google Antigravity) before revision; their findings and my adjudication are kept with the working files.
The training scripts, the attention rewrite, the benchmark and the checks are the instrument and can be published. The verdict and the trained adapters stay proprietary.
Claude Code drove the bench, so the drafting model's vendor also appears on the bench. A comment posted to MLX PR #4734 from the author's account was generated with Claude and says so.
The essay on this page was drafted by Claude Opus 5.5, an Anthropic model, from the lab’s frozen structured record under this practice’s voice specification, checked by a deterministic gate against every figure in the record, and validated by the author before publication. The record is the canonical surface: where prose and record disagree, the record rules.
Download the raw lab detail (Markdown)