Lab 026 · Editorial lab

On this fine-tune, the Mac Studio trained six times faster than a DGX Spark, and both adapters passed.

By Keith Townsend · October 20, 2026

The common assumption is that NVIDIA’s CUDA stack wins at fine-tuning. This lab trained the same LoRA adapter (a small set of extra weights trained on top of a model) for Lab 24’s clinic interpreter on two owned boxes: a DGX Spark, NVIDIA’s desktop GB10 system, and a Mac Studio with an M5 Ultra and 256 GB. Same data, recipe settings matched as far as the two trainers allow, scored the same way. The Studio finished in 17 minutes against 2.2 hours, and its adapter passed the same bar. It lost at one setting: a very narrow adapter that the Mac’s trainer didn’t learn as well as the Spark’s.

One model (Gemma 4 26B-A4B, bf16), one short-example LoRA job, one DGX Spark and one Mac Studio (M5 Ultra, 256 GB), one seed per run. The Spark ran Lab 18’s PyTorch and PEFT recipe in NVIDIA’s NGC container; the Studio ran Apple’s mlx-lm trainer. Software differs along with the hardware, so the speed ratio isn’t silicon alone. Power is each GPU’s rail as the system reports it, not wall-plug. A job with long sequences, or a dense model, may behave differently and is the next lab.

Do For short-example LoRA on a mixture-of-experts model this size, expect the Mac Studio to train faster than a DGX Spark, and confirm it on your own job. The Studio trained the rank-16 adapter in 993 seconds; the Spark took 8,056. With memory settings matched as far as mlx-lm allows, the Studio was 5.8 to 6.3 times faster, and the adapters cleared the same bar against the prompt: 0 and 1 consent misreads of 496, 7 and 4 avoidable hand-offs of 955.

Don’t Don’t assume a DGX Spark trains faster because it runs CUDA. On this job it didn’t, and memory capacity wasn’t the reason: the job needed about 70 GB, and with MLX’s limit set at 64 GiB the Studio trained in the same 16.5 minutes. Benchmark your own model on your own stack before you buy for training speed. Lab 20 found the Spark’s edge elsewhere: new models reach NVIDIA’s software first.

Don’t Don’t assume two trainers given the same recipe produce the same adapter. At rank 1 on attention only, the Mac trainer’s adapter failed the bar twice, on two seeds (42 and 56 consent misreads against the prompt’s 13), where the one Spark-trained run passed (0). Validation loss didn’t warn: it was 0.007 to 0.008. Score every adapter on your own test set before you ship it.

Do Use a wider adapter if you train on the Mac. Forge’s rank-1 recipe, spread across attention, the dense MLP and every expert, trained on the Studio in 20.5 minutes and ended 1,906 of 1,910 conversations the same way as Forge’s own adapter.

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 26B-A4B, 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 recipe and service time are a reference from Lab 24. Neither Apple, NVIDIA, CoreWeave nor Anthropic is a client. No vendor paid for this answer.

Training is supposed to be where NVIDIA's CUDA stack beats a Mac. So I trained the same LoRA adapter, a small set of extra weights on top of a model, on a DGX Spark and on a Mac Studio with an M5 Ultra. Same model, Gemma 4 26B-A4B. Same 2,872 examples from Lab 24's clinic interpreter. Recipe settings matched as closely as Apple's mlx-lm trainer allows. The Spark took 2.2 hours. The Studio took 17 minutes, and its adapter passed the same bar.

Did it pass?

Speed doesn't matter if the adapter is worse. So I tested both adapters the way Lab 24 did. The adapter reads patient messages for a clinic booking system and answers simple questions about them, like whether the patient agreed to a time. The bar it has to beat is the same model given a prompt with six worked examples per question.

Two mistakes count. The first is reading a "yes" the patient never gave, which books an appointment nobody agreed to. Out of 496 test conversations where that could happen, the prompt made 13 of those mistakes. The Studio-trained adapter made 0 and the Spark-trained one made 1. The second mistake is sending a patient to staff when the message was clear enough to handle. Out of 955 conversations, the prompt did that 16 times, the Studio's adapter 7 and the Spark's 4.

Both adapters beat the prompt on both counts. They also behaved almost identically: 1,901 of 1,910 conversations ended the same way with either one. On 320 questions held back from training, the Studio's got 300 right and the Spark's 308. That says both work. It doesn't prove they're equally accurate, because I didn't test for that.

Wasn't the Spark handicapped?

A little. The Spark recipe was built for long agent transcripts, so it saves memory by recomputing work instead of storing it. That costs speed. So I ran both boxes both ways, as far as mlx-lm allows. With the savings off, the Spark took 5,713 seconds. With them on, the Studio took 1,282. Matched that way, the Studio was 5.8 to 6.3 times faster. The settings explain 1.3 to 1.4x of the gap, and on the Spark the switch also changed the loss, so not all of that is memory.

Then I set MLX's memory limit on the Studio, first to the Spark's 121 GiB and then to 64 GiB. It trained in the same 16.5 minutes. The job needed about 70 GB. The extra memory isn't why the Mac is fast.

My guess is bandwidth. Each step on short examples reads a lot of weights and does little math, and Lab 22 put the Studio's published memory bandwidth at 4.4 times the Spark's. The gap here is bigger than that, so software is part of it. The Spark ran stock PyTorch code, not an optimized path for this model. I didn't try a faster Spark stack, and I didn't measure either box's bandwidth or compute, so treat it as a hypothesis. The Studio's GPU read about 94 W and the fan never came up.

Where it broke

Rank 1 is the result I'd read twice. At rank 1 on attention only, the narrowest adapter in Lab 24, the Mac's trainer failed. Twice, on two seeds: 56 and 42 consent misreads against the prompt's 13. The Spark's rank 1 passed with 0. Served by the same Mac runtime, the Spark's adapter answered 295 of 320 validation questions against the Mac's 243 and 258. So it's the training side, not the serving runtime. The Spark side is one run, though, so I can't rule out that it got a good draw.

And the Mac's validation loss looked fine, 0.007 to 0.008. Loss didn't tell me the adapter was worse. The scorer did.

I don't know yet what causes it. The two training recipes differ in several small ways, and I haven't tested them one at a time. One difference turned up while matching the recipes: Gemma 4 caps its output scores, and the Spark recipe's loss skipped that cap, so the Spark trained on uncapped scores and the Mac on capped ones. Clipping and loss averaging differ too. Width fixes it here. Forge's rank-1 recipe, spread across attention, the dense layers and every expert, trained on the Studio in 20.5 minutes and ended 1,906 of 1,910 conversations the same way as Forge's own adapter.

So what?

If you're fine-tuning a model like this on short examples, don't assume the CUDA box is faster. On this job the Mac Studio was, by about six times. Lab 20 found the Spark's edge somewhere else: new models land on NVIDIA's software first. Benchmark your own job before you buy for training speed. Does that hold for a job where the math starts to dominate? I'm running that now: Lab 18's coding-agent tune, a dense Gemma 4 31B trained on agent transcripts that average about 8,700 tokens, on both boxes, against Lab 18's own bar for whether the tune helped. That's the next lab. Whichever box you train on, score the adapter on your own test set before you ship it, because the loss won't tell you.

The numbers

Rank 16, wall time
Studio (mlx-lm) against Spark (PyTorch and PEFT, published Lab 24 run). 2,154 steps of 4. Studio time includes model load and four full validation passes, about 62 s; Spark time is the trainer’s own runtime, excluding load.
993 s against 8,056 s
Memory settings matched as far as mlx-lm allows
Savings off: Studio 993 s, Spark 5,713 s. Savings on: Studio 1,282 s (gradient checkpointing), Spark 8,056 s (checkpointing plus chunked loss). mlx-lm has no chunked loss, so savings-on mirrors half the Spark’s mode. On the Spark, savings off also switched to the stock loss, which applies the logit softcap and averages per token. One run per cell.
5.8x to 6.3x
Seconds per step, rank 16
Studio against Spark, savings off and on. Studio figures are wall time less validation passes, approximate.
0.43 to 0.57 against 2.65 to 3.74
Peak memory, rank 16
Studio process footprint (/usr/bin/time) of 256 GB, against Spark host memory used (free) of 121 GB, which includes about 7.7 GB of OS. Different counters.
58.6 to 69.9 GB against 92.5 to 107 GB
Studio under a memory cap
MLX memory and cache limits set before training, savings off. The limit is soft: at 64 GiB the process peaked at 69.7 GB. No swap.
994 s at 121 GiB, 987 s at 64 GiB
GPU-rail power while training
Studio: IOReport GPU Energy, read without sudo; CPU and DRAM channels read zero without privileges. Spark: nvidia-smi power.draw. Neither is wall-plug.
about 94 W against about 43 W
GPU-rail energy per rank-16 run
Studio savings off and on, measured. Spark savings off measured over the run including load; savings on extrapolated from a 15-minute probe at the published run’s length.
25.8 to 35.0 Wh against 69.6 to 95 Wh
Rank 16 against the fair prompt, frozen corpus
Studio-trained and Spark-trained. Fair six-example prompt: E 13, F 16. E counts consent misreads where the model’s “yes” counts (non-contestable pressure conversations); F counts avoidable hand-offs where code decides consent. Studio-trained, paired against the prompt: E 13 fixed, 0 new (p 0.0002); F 9 fixed, 0 new (p 0.004).
E 0 and 1 of 496 · F 7 and 4 of 955
Rank 16, control set
Studio-trained and Spark-trained. Fair prompt: E 8, F 6.
E 0 and 0 of 166 · F 3 and 3 of 294
Same outcome, Studio-trained against Spark-trained
Frozen corpus, rank 16; 91.3% of individual answers identical. Control set 584 of 588. The Mac serves greedily; the Spark arms used temperature 1.0, seeded, and a different runtime (vLLM).
1,901 of 1,910
Validation, same runtime (Studio, mlx-lm)
Rank 16, Studio-trained against Spark-trained, code fences stripped. Second runs: Studio 306 (checkpointing on), Spark 309 (savings off). Two Studio runs of the same job agreed on 302 of 320 answers.
300 against 308 of 320
Rank 1, attention only, validation same runtime
Studio-trained seeds 1 and 2 against Spark-trained.
243 and 258 against 295 of 320
Rank 1, attention only, frozen corpus
Studio-trained seeds 1 and 2 against Spark-trained. Fair prompt: E 13, F 16. Both Studio seeds fail the F bar and are worse than the prompt on E; the Spark-trained adapter passes.
E 56 and 42 · F 68 and 64, against E 0 · F 9
Forge’s recipe on the Studio
Rank 1, alpha 32, attention plus dense MLP plus every expert. Frozen corpus, against Forge’s own adapter on the same runtime at E 0, F 4; same outcome on 1,906 of 1,910. 74.8 GB peak, 27.7 Wh. Forge’s service took 1,852 s end to end in Lab 24, including provisioning, checkpointing and upload, on GPUs it doesn’t name.
1,231 s · E 0 · F 6
Trainable parameters, rank 16
115 attention modules: q, k, o on 30 layers, v on 25 (Gemma 4’s five full-attention layers have no v projection). PEFT also wraps 3,981,312 vision-tower parameters that text-only training never runs.
11,489,280 on both
Metered spend
Owned hardware throughout.
$0

Where each layer belongs

Read for portability. Could you take this layer elsewhere without rebuilding? How to read this table

LayerPlacement
Layer 0A · Compute
Compute
Mac Studio, mlx-lm trainer. Owned, open-source trainer, open weights converted with stock mlx-lm. The Spark-trained adapters ran on this runtime after an exact transpose, and the job ran on both vendors’ hardware, so nothing here ties the workload to Apple silicon. Follows Labs 21 and 24.
Retained
Layer 0A · Compute
Compute
DGX Spark, PyTorch and PEFT in NVIDIA’s NGC container. Commodity NVIDIA hardware is Retained by the instrument’s rule.
Retained
Layer 2C · Reasoning
Agentic Infrastructure — The Reasoning Plane
The trained interpreter adapters, from either box. Open weights, standard adapter files, translated between PEFT and mlx-lm by an exact transpose and checked against each other on the same runtime. Who decides an adapter is good enough stays with the operator’s own test set; validation loss didn’t catch the rank-1 failure.
Retained

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.

Data and scoring: Lab 24’s training split (2,872 examples, sha256 cf092844ecda) and validation split (320, 9d731f3125fd), byte-identical on both boxes; Lab 24’s frozen DCITL driver, scorer, 1,000-scenario frozen corpus and 300-scenario control set, unchanged; paired exact McNemar tests against Lab 24’s fair six-example prompt.

Spark: Lab 18’s train_lora_traj.py, unchanged, in NVIDIA’s NGC PyTorch 25.11 container, PEFT 0.18.0, rank 16 alpha 32 (and rank 1 alpha 2), dropout 0.05, 3 epochs, lr 1e-4 cosine, no warmup, micro-batch 1 times 4 accumulation, gradient checkpointing and a chunked loss. The savings-off run used a copy with exactly two changes: checkpointing off and the stock loss. A one-step memory canary ran first, and a watchdog would have stopped the container below 6 GB of available memory.

Studio: mlx-lm 0.32.2 on MLX 0.32.4, base converted to MLX bf16 with stock mlx-lm. Keys q, k, v, o on all 30 layers, scale 2.0 (alpha over rank), dropout 0.05, batch 4, 2,154 iterations, cosine decay to 0, AdamW with weight decay 0 and bias correction on (torch’s defaults), answer-only loss (mask_prompt), seed 1. Capped runs set MLX’s memory and cache limits before training. Forge’s recipe: keys extended to the dense MLP and the experts’ gate, up and down, rank 1, scale 32, dropout 0.0 (Forge’s adapter file).

Serving and evaluation: the Studio served each adapter with mlx_lm server, bound to localhost with thinking off; the harness ran from a separate Mac through an SSH tunnel, because bench code doesn’t run on a machine on the author’s personal network. Spark-trained adapters were translated to mlx-lm’s layout by an exact transpose, with every tensor and shape checked, to compare both trainers’ output on the same runtime.

Power: the Studio’s GPU Energy channel from IOReport, read without sudo; the Spark’s nvidia-smi power.draw every 5 seconds.

The training configs, translation, power and parity scripts are the instrument and can be published. The verdict and the trained adapters stay proprietary.

Claude Code drove the bench, and an Anthropic model drafted the essay, so the drafting model’s vendor also appears on the bench.

Reviewed before publication by a model from a different family (GPT-5.6-Sol, through the Codex CLI) instructed to attack the record. A second reviewer (Gemini, through the Antigravity CLI) didn’t return a review. The findings and their disposition are kept with the lab’s working notes.

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)