Agentic coding on a laptop with Pi and a local LLM
On my AMD Ryzen AI 9 HX 370 laptop, which has a Radeon 890M iGPU — unified memory — I switched to using a local LLM for development, replacing the cloud models I'd normally reach for. The lean Pi coding agent is also essential for this to work. If you've been wondering whether a local model can really do the job of building a big application, here's what I found.
The setup
The project I am working on is a distributed communication system, split into two Rust codebases:
- a Dioxus frontend, running as WASM in the browser — of roughly 35,000 lines of code;
- an Axum backend running on one or several servers — of roughly 11,000 lines of code.
The model that drives the agent is Tiel-Coder-35B-A3B-MTP:UD-Q4_K_XL
a quantised GGUF model served locally by
llama.cpp. The model's page
is on
HuggingFace.
It runs on a single NixOS laptop — an AMD Ryzen
AI 9 HX
370
with its integrated Radeon
890M
GPU — and the whole thing, model and all, takes about 28 GiB of
unified memory, so inference never leaves the host. The GPU backend is
Vulkan.
The start script
The model is launched with llama-server from a small shell script.
The script takes the port as its only argument. Here it is in full.
1#!/bin/sh
2#
3# https://huggingface.co/peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP
4#
5
6export MODEL="/home/mdo/.models/Tiel-Coder-35B-A3B-MTP-UD-Q4_K_XL.gguf"
7export MODEL_NAME="Tiel-Coder-35B-A3B-MTP"
8
9export CONTEXT_WINDOW_SIZE=262144
10
11# https://github.com/ggml-org/llama.cpp/issues/13178
12# This MTP model has a jinja templated bundled.
13# export QWEN3_CODER_TEMPLATE="./Tiel-Coder-35B-A3B-MTP-UD.jinja"
14
15# BATCH TUNING FOR iGPU:
16# --batch-size: Logical limit for prompt processing (2048 is safe)
17# --ubatch-size: Physical chunk size processed by the GPU at once.
18# For RDNA3.5 (890M), 512 is the "sweet spot" for speed without driver hangs.
19export BATCH_SIZE=512
20export UBATCH_SIZE=512
21
22# Environment: Force Vulkan to see more of your RAM if needed.
23export GGML_VULKAN_DEVICE_MEMORY_LIMIT=32768
24
25# OPTIMIZE FOR 890M SHARED MEMORY PIPELINE
26export GGML_VULKAN_UNIFIED_MEMORY=1
27
28# 1. FORCE RADV TO TREAT ALL RAM EQUALLY This stops the driver from
29# "moving" data from System RAM to VRAM
30# export GGML_VK_PREFER_HOST_MEMORY=1
31export GGML_VK_PREFER_HOST_MEMORY=1
32
33# Avoid suballocation pool fragmentation
34export GGML_VK_SUBALLOCATION_BLOCK_SIZE=4294967296
35
36# Port as parameter
37if [ $# -ne 1 ]
38then
39 echo "Usage: $0 <port>";
40 exit 1
41fi
42
43export PORT=$1
44
45GGML_BLAS_DEVICE=Vulkan0 GGML_BACKEND=Vulkan llama-server \
46 -m "${MODEL}" \
47 --alias "${MODEL_NAME}" \
48 --spec-type draft-mtp \
49 --spec-draft-n-max 3 \
50 --spec-draft-p-min 0.8 \
51 --host 0.0.0.0 \
52 --port ${PORT} \
53 --load-mode mmap+mlock \
54 -n -1 \
55 -ngl 100 \
56 -np 1 \
57 --cache-prompt \
58 --flash-attn on \
59 -c ${CONTEXT_WINDOW_SIZE} \
60 --cache-type-k q8_0 \
61 --cache-type-v q8_0 \
62 --batch-size ${BATCH_SIZE} \
63 --ubatch-size ${UBATCH_SIZE} \
64 --jinja \
65 -t 24 \
66 -tb 24 \
67 --temp 0.6 \
68 --top-p 0.95 \
69 --min-p 0.01 \
70 --repeat-penalty 1.0 \
71 --presence-penalty 0.0 \
72 --frequency-penalty 0.0 \
73 --no-mmproj
A few things are worth pointing out. The mixture-of-experts
(MoE) model uses multi-token
prediction
(MTP), a
flavour of speculative decoding. Rather than training two separate
models, the model has extra 'assistant' heads trained on some of its
intermediate layers, so those layers can act as a small draft model
entirely on their own. That draft model proposes a few tokens at once,
and the full model verifies them in a single parallel pass: accepted
tokens are kept, and the first rejected token and everything after it
are discarded. In the start script, this is what --spec-type draft-mtp selects; --spec-draft-n-max 3 caps the draft at three
tokens ahead; --spec-draft-p-min 0.8 sets the minimum probability a
draft token must have to be accepted. The context window is a generous
262,144 tokens, which is what lets the agent hold a large slice of
the codebase in one prompt.
Pi configuration
The Pi coding agent runs in its dedicated Docker
container — see photonsphere /
ai-containerized
— and is configured to use the locally running model (which I run on
port 8092 — remember to open up the port in your firewall).
~/.pi/agent/models.json:
1{
2 "providers": {
3 "llama-server-8092": {
4 "baseUrl": "http://host.docker.internal:8092/v1",
5 "api": "openai-completions",
6 "apiKey": "none",
7 "compat": {
8 "supportsDeveloperRole": true,
9 "supportsReasoningEffort": true,
10 "maxTokensField": "max_tokens"
11 },
12 "models": [
13 {
14 "id": "0.0.0.0:8092",
15 "name": "8092",
16 "reasoning": true,
17 "input": ["text"],
18 "contextWindow": 262144,
19 "maxTokens": 131072,
20 "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
21 }
22 ]
23 }
24 }
25}
Note that contextWindow and maxTokens must be set high enough to
accommodate the agent's long reasoning turns.
How fast is it, really?
Generation throughput per task — tg (tokens/second or t/s) as
reported by llama-server, over the two log captures:
| Metric | Client log | Server log |
|---|---|---|
| Samples | 2,846 | 2,100 |
Mean tg |
11.76 t/s | 13.79 t/s |
Median tg |
≈ 10.3 t/s | ≈ 14.2 t/s |
| Min / Max | 6.63 / 29.53 | 7.52 / 24.81 |
| Samples ≥ 20 t/s | 16 (0.6 %) | 35 (1.7 %) |
| Samples < 15 t/s | 2,189 (77 %) | 1,199 (57 %) |
| Mean of first 60 samples¹ | 19.45 t/s | 18.46 t/s |
¹ The "first ~60 samples" cover the opening minutes of a session, before contexts (applies to both client and server) grew.
Prompt processing (the agent re-reading its context) is the flip side. The worst cases:
| Log | Tokens | Time | Throughput |
|---|---|---|---|
| client | 115,236 | 924.3 s (~15.4 min) | ~125 tok/s |
| client | 22,544 | 277.4 s | ~81 tok/s |
| client | 16,579 | 242.7 s | ~68 tok/s |
| server | 71,012 | 442.8 s (~7.4 min) | ~160 tok/s |
| server | 34,219 | 371.1 s | ~92 tok/s |
It pays to start a fresh session per task, because the agent runs much faster with a smaller context.
Guiding instructions
Some guiding instructions shaped every session. The 'restraint' rules were explicit: write idiomatic Rust, prefer a test-driven approach where tooling permits, never commit to version control, stop and report a problem rather than get stuck in analysis paralysis or paper over it, avoid spaghetti code in favour of a scalable architecture. The 'role' instructions cast the model as an experienced Rust developer and software architect, well versed in Dioxus, WASM FFI (JavaScript), and Axum, among other things.
Is it feasible?
Yes — and on nothing more than a single laptop, at that. A large and complex project can now be developed with a local model as the sole peer programmer. I have already made the full switch.
The model used here is small enough to run entirely on one workstation
and fast enough to stay in the developer's flow. The model is
fine-tuned for coding only and its 'coding-fu' genuinely feels on par
with some cloud models such as MiMo-V2.5. Running a model locally,
however, drains your laptop's battery quickly, so keep it plugged in
when you can; also, clean the fans regularly, since the high fan load
causes dust to accumulate faster.
Quoting the model itself
Ugh, I'm going in circles. Let me just try the restructure and see if it compiles. Empirical iteration is faster than theorizing.