Porting nanochat to a TPU: what carries over from PyTorch, and what breaks A developer ported Karpathy's nanochat model to run on Google TPU v6e-8 using JAX, achieving comparable training quality but only half the hardware utilization (24% MFU vs. 47-48% on H100). The port required adapting PyTorch code for TPU-specific optimizations like Splash attention kernels and padding tensors to multiples of 256 for the MXU. Porting nanochat to a TPU: what carries over from PyTorch, and what breaks 1 tucan9389 /tucan9389 announced in Announcements /tucan9389/nanochat-jax/discussions/categories/announcements - | Karpathy's nanochat normally runs on an 8×H100 GPU node, and several ports of it to JAX already exist. Among them, my aim was to keep the config and architecture as close to nanochat as possible 1. Reproduction resultsnanochat-jax currently provides a speedrun.sh script that covers base model training and SFT upstream nanochat goes all the way to RL; this reproduction stops at SFT . The claim above — that the quality reproduced — is based on the CORE score. On performance, there's still a gap. MFU is about 24% d24 — half of Karpathy's measured H100 numbers 47–48% at d20 . Below, we run each script of speedrun.sh on a TPU v6e-8 and check whether the quality Karpathy reported actually comes out. If you spot anything wrong or unclear in this post, or have a question, please let me know at 2. TPU basicsThis run used The v6e's MXU grew to 256×256, from the 128×128 of every generation up to v5p — if a tensor dimension isn't a multiple of 256, XLA pads it with zeros and part of the unit is wasted more on this in insight 5 of section 4 . Meanwhile, HBM is 32GB per chip — a third of v5p's 95GB — while compute is 2×: a compute-heavy, memory-lean design. Our v6e-8 slice adds up to 7.34 bf16 PFLOPS and costs ~$4–5/hour on Of this stack, nanochat-jax uses three software layers — XLA, JAX, and Flax — plus Pallas, which isn't in the figure. 3. Speedrun verification — from the tokenizer to the report cardThe pipeline runs tokenizer 5.3m → base 6.02h + eval 44.5m → SFT 68.7m + eval ~3.5h , and we run the scripts that speedrun.sh executes, one stage at a time to run everything in one shot, see Appendix C . For a deep understanding of each stage, see Karpathy's Step 1. Tokenizer 5.3m python -m nanochat jax.dataset -n 170 ClimbMix train 170 + val 1 shards python -m scripts.tok train --max-chars 100000000 --doc-cap 10000 --vocab-size 32768 python -m scripts.tok eval We train a vocab-32768 train row , ours uses 1.5% fewer tokens than the GPT-2 tokenizer. Fewer tokens means more text learned for the same budget — a compression advantage — and Karpathy's tokenizer shows the same pattern, a signal that the reproduction is on track for Korean, the training data contains no Hangul, so ours spends about 2× the tokens of GPT-4; the full per-domain table is in Appendix D . The number to check: train +1.5%. Step 2. Base model 6.02h + 44.5m --recipe through --use-real-data: Karpathy's R4 config / --attn-impl through --splash- : v6e-8 TPU-specific python -m scripts.base train \ --recipe=324e69c --depth=24 --seq-len=2048 --vocab-size=32768 \ --target-param-data-ratio=9.5 --total-batch-size=1048576 \ --device-batch-size=2 --grad-accum-steps=32 --grad-accum-impl=fused \ --warmup-steps=0 --warmdown-ratio=0.5 --final-lr-frac=0.0 \ --weight-decay=0.2 --matrix-lr=0.02 --embedding-lr=0.3 \ --unembedding-lr=0.004 --scalar-lr=0.5 \ --bf16 --cast-embeddings-bf16 --use-real-data \ --attn-impl=splash --splash-block-q=512 --splash-block-kv=512 --splash-block-kv-compute=256 \ --matmul-precision=default --lm-head-precision=highest --ve-grad-impl=onehot \ --checkpoint-every=200 --keep-last-checkpoints=2 \ --model-tag=d24 speedrun r4 --no-final-eval The top half of these arguments is the R4 recipe as-is 730M scaling parameters × 9.5 ≈ 6.9B tokens --splash- kernel block sizes. step 100 loss=4.517054 ... mfu=24.5% step 1000 loss=2.786205 ... mfu=24.7% Against Karpathy R4's 2-hour base train loop 8×H100 , ours takes 5.29h 6.02h wall clock including checkpointing and compilation , at about 24% MFU The two lines above are an excerpt; I've uploaded the RESULTS DIR=$HOME/.cache/nanochat-jax/results/d24 speedrun r4 && mkdir -p $RESULTS DIR python -m scripts.base eval --source base --model-tag d24 speedrun r4 \ --eval bpb,core --eval-steps 20 --device-batch-size 1 --max-per-task -1 \ --out $RESULTS DIR/base eval.json --partial-out $RESULTS DIR/base eval core partial.json --bf16 CORE the 22-task metric from section 1 comes out to Step 3. SFT 68.7m + eval ~3.5h SFT training 68.7m python -m scripts.chat sft --model-tag d24 speedrun r4 --output-model-tag d24 speedrun r4 sft \ --num-iterations -1 --device-batch-size 1 --max-seq-len 2048 --total-batch-size 524288 \ --sft-scope full --eval-every 200 --eval-steps 4 \ --chatcore-every -1 --chatcore-max-cat -1 --chatcore-max-sample 24 \ --load-optimizer 1 --log-every 10 --bf16 scoring — the 3 multiple-choice tasks in one go, the 3 generative tasks one by one python -m scripts.chat eval -i sft -g d24 speedrun r4 sft --max-new-tokens 512 --batch-size 8 \ --dtype bfloat16 --out $RESULTS DIR/chat eval sft.json --task-name "ARC-Easy|ARC-Challenge|MMLU" for task in SpellingBee HumanEval GSM8K; do python -m scripts.chat eval -i sft -g d24 speedrun r4 sft --task-name "$task" \ --max-new-tokens 512 --dtype bfloat16 --num-samples 1 --temperature 0 --top-k 50 \ --jit-gen 1 --out $RESULTS DIR/chat eval $ echo "$task" | tr 'A-Z' 'a-z' .json done Train for 68.7 minutes on a mixture ~1.07M rows of SmolTalk, MMLU, GSM8K, SpellingBee , and the base model that could only repeat the question back becomes a model that answers it. ChatCORE the centered average over 6 chat tasks is 0.3733 — the comparison line here is the base model itself. On the same 6 tasks, base scored essentially 0 on the generative ones, so most of the generative score is ability SFT added: SpellingBee 0.9961, GSM8K 0.1008 math is RL's job . I don't compare 1:1 against the report card in Karpathy's walkthrough — that one is d20 and ran on the older midtraining pipeline. Beyond the scores, checking with actual prompts: JAX PLATFORMS=cpu NANOCHAT JAX BASE DIR=