daily

2026-08-01
1

Run Kimi K3 using 29 GB of RAM at 0.50 tok/s

Hacker News · original → · 8/10 · AI/work: LLM inference optimization on consumer hardware
Kimi K3 — 2.78 trillion parameters — running on a consumer laptop. $ waste run ~/models/k3.waste 'What is the capital of Italy?' waste: no --budget, using 46.24 GB of 64.00 GB (expert cache 17.56…

Kimi K3 — 2.78 trillion parameters — running on a consumer laptop. $ waste run ~/models/k3.waste 'What is the capital of Italy?' waste: no --budget, using 46.24 GB of 64.00 GB (expert cache 17.56 GB) The capital of Italy is **Rome**. [16 tokens, 31.09 s, 0.51 tok/s | experts 3357 hit / 20195 miss = 14%] WASTE is an embeddable inference engine written in C, with no third-party runtime dependencies. It keeps the model trunk in memory, streams selected experts directly from disk, and uses the remaining RAM as a bounded expert cache. Its current proof point is the complete open-weights Kimi K3 model: 2.78 trillion parameters, converted into a 982 GiB container and running on a 64 GB MacBook Pro at 0.49–0.54 tokens per second. This is not a distilled, pruned, or reduced variant. | Model | Container | Minimum RAM | Tested speed | |---|---|---|---| | Kimi K3 2.78T | 982 GiB | 29.05 GiB | 0.49–0.54 tok/s | | Kimi-Linear 48B | 19 GiB | 1.87 GiB | 10.7 tok/s | WASTE was written for that one model and that one constraint: K3 does not fit in the RAM of current mainstream consumer systems. It is 1.42 TB as published and 982 GB after conversion. But a mixture of experts activates about 4% of itself per token, so almost all of that weight is idle at any instant — and idle weight does not need to be in memory, it needs to be reachable in time. WASTE keeps it on disk in a layout where one expert costs exactly one read, streams what each token actually needs, and spends every remaining byte of RAM on the part that repeats. The engine is correct: every layer is validated against a PyTorch reference, the final logits agree to 3.6e-06, and the vision tower matches its own oracle to 2.3e-06. It is also slow — half a token per second, thirty seconds for the sentence above. Both of those matter, and the second one should not be read as a disclaimer. We are not aware of another published demonstration of a model this size streaming from disk on a consumer machine: we found none for trillion-scale NVMe streaming, and the best-documented 671B-class recipes assume a server with a terabyte of DDR5. That is a report of what our search turned up rather than a survey — this repository carries no bibliography and no comparison table, so read it as an invitation to send a counter-example, not as a result. The interesting part is not the speed, it is that the whole thing is in the reachable range on a single consumer machine — and that from here the question is engineering rather than feasibility. Where the levers were is not where they are. Overlapping the expert reads with the arithmetic was worth ~1.6x and shipped; the two that looked bigger — reading fewer bytes per token, and keeping more of them in RAM — were both measured and both refused, one because this family's router has no tail to demote and one because a cache the machine will not leave resident cannot be bought at any price. Even with the reads overlapped they are still 55% of a decode step against the arithmetic's 27%, so what is left is a faster disk or a machine with more RAM, not another pass over the kernels. docs/EFFICIENCY.md is the account of how each of those was priced, including the two that were built before being measured. What that opens up, concretely: a frontier-scale model that answers with no network, no per-token invoice, and nothing leaving the machine — which is the difference between "you may not send that data to an API" and "run it here". The format and the engine are not K3-specific in any deep way; K3 is simply the hardest case that exists today, and a model that streams at 2.78T streams comfortably at 48B. Every number in this document was measured on the commit it is published with, and the ones that were wrong are recorded as wrong in docs/LEARNED.md rather than quietly corrected. Every token answered by a cloud service is paid for twice: once on the invoice, and once in the electricity of a datacenter running a model that would fit — barely, awkwardly, but genuinely — on hardware already sitting on a desk. WASTE means to be the first concrete step toward ending that waste of tokens. The acronym came second. | disk, for the model | 982 GB for the converted container — plan a terabyte | | disk, to convert it | another 1.42 TB of staging for the published shards, freed afterwards | | RAM | 29.05 GB minimum to open K3 at 4K context; 64 GB for the numbers here | | storage speed | the container must be on internal NVMe — see below | | build | a C11 compiler and make . No BLAS, no CUDA, no Python at run time | Sizes here are powers of two, the way df and the engine both report them: the container is 982 GiB, which a disk vendor would call 1.05 TB. The RAM floor is what the engine refuses to start below, and it is almost entirely the 27.28 GB resident trunk. Useful throughput starts higher: on a 64 GB machine the engine gives itself a 46 GB budget, of which 17.56 GB is expert cache, and that is the top of the measured curve. A 32 GB machine can technically open the model and will page badly; treat 64 GB as the real requirement. Storage speed is not a detail. A token reads 17 GB of experts. On the internal SSD that is 12.78 GB/s and the model streams; over a USB enclosure it is 0.94 GB/s and the same token takes thirteen seconds. Convert onto internal NVMe, and use the external disk for the download only. If a terabyte is not available, the same engine and the same format run Kimi-Linear-48B-A3B-Instruct from a 19 GB container with a 1.87 GB floor, at 10.7 tok/s. That is the good path for trying WASTE out before committing a disk to K3. - Self-contained. One libwaste.a , onewaste binary, nothing at run time beyond libc and pthreads. - Zero dependencies. No BLAS, no ONNX, no Python in the inference path, nothing to install. The Python under tools/ converts models and validates the engine; it never runs alongside it. - Fully embeddable. Twenty-six public functions in src/waste.h: open a model under a RAM ceiling, generate, save the session, close. The CLI is a client of that API and touches nothing private — if the CLI can do it, so can an embedding host. waste_cfg cfg; waste_cfg_init(&cfg); cfg.ram_budget_bytes = 46ULL << 30; /* a hard ceiling, not a hint; 0 sizes it to this machine */ waste_ctx *ctx; if (waste_open("/path/to/k3.waste", &cfg, &ctx) != WASTE_OK) return 1; waste_generate(ctx, ids, n, ¶ms, on_token, user); waste_close(ctx); The path is the container directory the converter wrote — no ~ expansion here, that is the shell's job. A model is converted once into a .waste container: a JSON manifest, a resident trunk, and one expert bank per layer. Each expert record is 4 KiB-aligned with its gate, up and down matrices adjacent, so routing to an expert costs exactly one pread — not three, not a seek per matrix. The arithmetic was never the bottleneck. Reads bypass the page cache (F_NOCACHE on macOS, O_DIRECT on Linux, FILE_FLAG_NO_BUFFERING on Windows). That is deliberate: with a container smaller than RAM the kernel would cache everything, and the hit rates measured that way are a fiction that does not survive contact with a 982 GB model. Every record's header is checked on the way in — right magic, the expert the index asked for, offsets that fit — so a bank that has been truncated or spliced stops the generation and names the record instead of answering from the wrong bytes. That costs nothing measurable. The record also carries a crc32 over its payload, and checking that is --verify , off by default: it is a pass over every record on every cache miss, about 5% on Kimi-Linear and 1% on K3. Worth it for a container you copied or downloaded and have not read since; not worth it on every token of one you converted yourself. See docs/FORMAT.md. Experts are stored as residual vector quantization — three stages of 256-entry codebooks over 8-dimensional vectors, 3.00 bits per weight — and the matrix is never materialized. For each token the engine builds a table of partial dot products, one per codebook entry per vector position, after which every expert row is three table reads and two adds. The trunk stays at 4 and 8 bits. The model was trained with quantization-aware training on the experts only, so it has no trained tolerance for a squeezed trunk: a 3-bit trunk was built and measured, the cache prediction held, the throughput did not, and the output collapsed. The most predictive number in this project. K3 touches 16 experts in each of 92 layers per token: 17.0 GB. Below that, an expert cached for one token is evicted before the next token asks for it, and the hit rate is not low — it is zero. Above it the curve bends sharply. | budget | expert cache | hit rate | decode | |---|---|---|---| | 32 GB | 3.32 GB | 0% | 0.31 tok/s | | 46 GB | 17.32 GB | 13% | 0.32 tok/s | | 52 GB | 23.32 GB | 27% | 0.11–0.14 tok/s | | 58 GB | 29.32 GB | 37% | 0.04 tok/s | Measured in that order, on an otherwise idle machine. Order matters: re-run after the 52 and 58 GB rows have driven the machine into paging, 46 GB gives 0.22–0.25 rather than 0.32 — while reporting hit and miss counts identical to the digit. The engine is deterministic; the machine is not, and it does not fully recover between runs. Sweep upward. The decode column predates read-ahead and has not been re-swept: 46 GB now runs at 0.51 rather than 0.32. The shape is what the table is for, and read-ahead does not move it — it hides I/O behind arithmetic, which makes every row faster and none of them a different budget. Everything in the memory design exists to get above that line, which is why the engine works to free RAM rather than to save it. And there is a ceiling on the other side, closer than it looks. Read that table twice: the hit rate climbs all the way down. At 58 GB on a 64 GB machine the cache serves 37% of experts from RAM and the engine is eight times slower than at 46 GB, where it serves 13%. The engine is inside its budget; the machine is not, so the OS pages out the expert cache, and a "hit" becomes a page fault instead of the disk read the engine was managing. So the usable window is narrow. It opens at ~46 GB, where the cache finally clears one token's working set, and it has already closed by 52 — on an otherwise idle machine, with 49 GB free before the run. It is also sharp enough to move under a change that looks unrelated: taking 1.11 GB of embedding table off the resident set fed straight into the cache at a fixed budget, and that was enough to push 58 GB from 0.32 tok/s to 0.04. So the default does not fill the machine. Expert cache is only worth anything in whole multiples of that working set, and the remainder above a multiple buys a few points of hit rate while pushing the machine towards paging. When it picks a budget for itself the engine steps down a whole working set at a time and takes the largest that fits under seven eighths of RAM: K3 asks for floor + 3× — 80.63 GB — and gets floor + 1× on this laptop, a 46 GB budget and a 17.56 GB cache. That is the top of the curve above, reached with no flag. A 128 GB machine still gets the full 3×. An earlier version took every byte up to the cap instead, which put a 27 GB cache on this machine — between two budgets measured at 0.11 and 0.04 tok/s. The real lesson is that a cache you do not control is not a cache, and the corollary is that an engine should stop asking for memory before the OS starts taking it back. K3's attention is a 3:1 hybrid: Kimi Delta Attention, which carries a fixed-size recurrent state instead of a growing KV cache, and gated multi-head latent attention. The MLA layers cache the 512-wide latent rather than expanded per-head keys and values, with kv_b_proj absorbed into the query and the output: q_nope · (W_kb c) == (W_kbᵀ q_nope) · c Σ_s a_s (W_vb c_s) == W_vb (Σ_s a_s c_s) Identical logits to 1.2e-05, and 53× less cache: 11.25 GB becomes 0.21 GB at 4K context. It is also what makes long context possible at all — the expanded layout wants 360 GB at 128K tokens, the latent one 7.2. MacBook Pro M5 Pro, 64 GB, container on the internal SSD. Every figure was measured on the commit it is published with. | minimum RAM | 29.05 GB at 4K context | | 30.54 GB at 32K, 35.63 GB at 128K, 83.21 GB at 1M | | | resident trunk | 27.28 GB | | read per token | 17.0 GB, read ahead on two threads so it overlaps the matmuls | | model load | 20 s | | prefill | 0.47 tok/s chunked, 0.29 sequential (before read-ahead) | | decode | 0.49–0.54 tok/s at the default budget, the best this machine gives | | vision tower | 15.7 s for a 1024-patch image, 27 layers | | image in a prompt | 256 positions for 896x896, 2.8 s each — as text | The floor is almost entirely the resident trunk. Useful throughput starts above ~46 GB, where the expert cache finally clears one token's working set, and is gone again by 52, where the machine starts paging. Below the first line extra RAM buys nothing; above the second it costs, badly. The window is one budget wide on this machine. The tower is not what an image costs. Encoding 1024 patches takes 15.7 s; the 256 positions it produces then go through the 92 MoE layers like any other token, which is the other 731 s. An image is priced as text of the same length, so the patch budget in vision.json is a real dial: halving the grid halves the prompt. | minimum RAM | 1.87 GB | | decode | 10.7 tok/s at an 8 GB budget, 78% cache hit | The same engine and the same format, on a model that fits comfortably. This is what WASTE looks like when it is not fighting. Decode on K3, 17.32 GB of cache and still cold — 6.7% hit over ten steps, which is the state a fresh prompt starts in: | share | | |---|---| | MoE, all of it | 82.5% | | of which expert I/O | 53.5% | | of which expert matmul | 20.0% | | KDA layers | 14.5% | | MLA layers | 2.8% | | lm_head | 0.2% | Reproduce with WASTE_PROFILE=1 WASTE_CACHE_MB=17735 ./test_forward MODEL 1008,10484,318,15383,387 out.bin 5 . The I/O share falls as the cache warms, so a long session sits lower than this; the ranking does not change. The I/O already runs near the hardware limit — 17.0 GB per token at ~9.9 GB/s against the SSD's measured 12.78 — so it only gets cheaper by happening less often, which means cache, which means RAM. That is the whole optimization story so far, and the reason the next steps are about memory rather than arithmetic. git clone https://github.com/sqliteai/waste && cd waste make # libwaste.a, waste, libwastevq make check # 23 pass, 11 skip on a fresh clone No configure step and no dependency resolution. make check needs no model: it builds a small synthetic container and runs the engine against it. The eleven skips are the checks that need something a clone does not carry — the PyTorch oracle, the round-trip against the source shards, anything driving the CLI with text, since the synthetic container carries no tokenizer, and the K3 checks, which want the container and the release on disk. With both containers present the suite is 36 checks. Conversion is the one step that needs Python, and it happens once. The source is moonshotai/Kimi-K3 exactly as published — 96 safetensors shards, 1.42 TB, nothing patched: # 1. preflight: reachable? how big? does it fit? tools/fetch_weights.sh --dest /Volumes/staging/k3 --dry-run # 2. download — resumable, safe to kill, safe to re-run tools/fetch_weights.sh --dest /Volumes/staging/k3 # 3. convert into a container uv run --with torch --with safetensors python tools/convert.py \ --src /Volumes/staging/k3 \ --out ~/models/k3.waste --jobs 3 That produces the 982 GB container every number above was measured on. It takes about 4.7 hours with three processes on the M5 Pro (23.7 with the pure-torch encoder — see docs/K3.md), and wants ~1.0 TB free on the target volume. The converter is resumable too: a layer whose bank is already written is skipped, so an interrupted run costs only the layer it was in the middle of. The download is the part that goes wrong. A 1.42 TB pull over hours will hit dropped connections, CDN 5xx and at least one interrupted run, so every shard resumes mid-file rather than restarting, retries with exponential backoff and jitter, and counts as done only when its size matches Content-Length — recorded in a state file, so a re-run skips finished shards without even a HEAD request. --check re-verifies everything on disk against the remote and downloads nothing (96 shards in 34 s). --repo points it at another model, HF_TOKEN at a gated one. macOS and Linux. Give --dest a staging disk rather than the volume that will hold the container. The shards are read once, by the converter; the container is read continuously, at every token. On this machine the external enclosure measures 0.94 GB/s against the internal NVMe's 12.78 — see docs/GATES.md, Gate H — which is the difference between a model that streams and one that stalls. tools/pipeline.sh chains the whole thing unattended — download, convert, round-trip the container against the source weights, generate, then diff the logits against the PyTorch oracle — and leaves a report next to the container. The same converter handles the other member of the family, Kimi-Linear-48B-A3B-Instruct , into the 19 GB container of the second benchmark; --src is the only thing that changes. Pre-converted containers are on their way to huggingface.co/sqliteai, at which point this whole section becomes a download and the Python is only needed for models we have not published. The container is the directory the converter wrote, so give it that path — ~/models/k3.waste throughout this README: waste run ~/models/k3.waste "The capital of France is" -n 32 waste chat ~/models/k3.waste # multi-turn, state kept waste eval ~/models/k3.waste "2 + 2 =" --top-k 5 # next-token distribution waste plan ~/models/k3.waste --budget 46G # what fits, what does not echo "prompt" | waste run ~/models/k3.waste # stdin works too -n is a cap, not a requirement: without it generation stops at the container's end-of-sequence token or at 128 tokens, whichever comes first. The examples pass it because 128 tokens of K3 is six minutes. --budget is optional, and leaving it out is the right default rather than a fallback: the engine takes the container's recommendation, steps it down a whole token working set at a time until it fits under seven eighths of physical RAM, and never goes below the floor — a budget you set explicitly under the floor is refused rather than swapped into. It then says on stderr what it landed on, so the same command on two machines is not silently two different runs: waste: no --budget, using 46.24 GB of 64.00 GB (expert cache 17.56 GB) --verify checks each expert record's crc32 as it comes off the disk. It is off by default, and that is a throughput decision rather than a claim that containers do not rot: it is a pass over every record on every cache miss, about 5% on Kimi-Linear and about 1% on K3, where the read dominates. Turn it on once for a container you copied, downloaded, or left on a disk you do not trust, and for anything whose wrong answers would be believed; leave it off for one you converted yourself and have been reading since. WASTE_VERIFY=1 in the environment does the same thing, and the server takes --verify as well. Any of them turns it on; none of them turns it off. What is checked either way: a short read, and a record header that does not describe the expert the bank index asked for. Those are O(1), they cost nothing measurable, and they are what keeps a damaged offset out of the arithmetic — --verify only adds the pass over the payload. waste --help lists all nine commands. --json makes eval , tokenize , plan , info and bench machine-readable. serve/ is an OpenAI-compatible HTTP server — the second client of the public API, alongside the CLI, reaching the same engine through ctypes rather than keeping a copy of the model code in Python: make libwaste.dylib # or libwaste.so on Linux python3 -m serve ~/models/k3.waste --port 8000 curl localhost:8000/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{"model":"k3","messages":[{"role":"user","content":"Why is the sky blue?"}]}' /v1/chat/completions (streaming and not), /v1/completions , /v1/models , /health . It carries the whole of K3's prompt format, not the four-string subset a container's chat.json can hold: tool definitions and tool results, typed call arguments, JSON response schemas, tool_choice , the think channel and thinking_effort , and images — plus the parser that reads the reply back into reasoning, answer and tool_calls . Stdlib only. The prompt renderer is a port of encoding_k3.py from the release, and the test suite checks it against that file segment for segment on a corpus of 38 conversations whenever the weights directory is on disk. docs/SERVE.md is the reference. K3 is multimodal — a 401M ViT, 27 layers, patch 14 — and so is the engine. --image attaches a picture; repeat it for several: $ waste run ~/models/k3.waste 'What is in this picture?' --image landscape.png [landscape.png: 192 image tokens] The picture shows a simple, stylized landscape with: - A **blue sky** with a gradient from darker blue at the top to lighter blue near the horizon. - A **yellow sun** in the upper right. - A **gray hill or mountain** in the middle distance. - A **green field** covering the lower part of the image. [78 tokens, 234.25 s, 0.33 tok/s | experts 15314 hit / 99502 miss = 13%] (That transcript predates read-ahead and its timing line is the old one — the picture it was run against is not in this repository, so it is left as recorded rather than re-timed. Same tokens, about 1.6x less waiting.) That is a 448×336 image, and every element of the description is in it — including the sky gradient, which is the kind of detail that separates a tower that works from one that merely runs. The picture was generated by a twenty-line script rather than photographed, so the answer can be checked against what was drawn instead of against an impression. PNG, JPEG, GIF, BMP, TGA and PSD, decoded by the one vendored header in third_party/ . It works on run , chat and eval ; inside a chat, /image FILE attaches a picture to the next message, and it is spliced once — the positions are in the attention state afterwards, so later turns discuss the same photograph without re-encoding it. The 27-layer ViT is loaded only when an image is present. Its weights are 434 MB, but the reservation is 1.12 GB: the bounded source decode, the tower's activations and the queued image embeddings are memory too, and all of it otherwise comes straight out of the expert cache. An image is not one token. The tower turns a 14-pixel patch grid into one embedding per merged 2×2 patch, and each occupies a position in the sequence — the 448×336 above is 192 of them, an 896×896 photo at the default budget is 256. That is worth knowing before wondering where a context window went, and it is most of what an image costs: the 234 s in the transcript is the 78 generated tokens alone, and the picture is paid for before that, in prefill. An image is priced as text of the same length. The tower is the cheap part — 15.7 s for a full 1024-patch image — and its output then walks through 92 MoE layers like any other token. Halving max_patches in vision.json halves the bill. Through the library it is three calls, because a host needs to size the prompt before committing to it: size_t rows; waste_image_add(ctx, "photo.png", &rows); /* encode and queue */ waste_image_expand(ctx, raw, n, ids, cap, &n_ids); /* placeholder -> N */ waste_generate(ctx, ids, n_ids, ¶ms, cb, u); /* consumes the queue */ The tower's shape, the patch budget and the pixel normalization live in vision.json , which the converter writes from the release's own nested vision_config and from preprocessor_config.json . K3 normalizes to [-1, 1] with mean = std = 0.5. That last sentence was wrong here for a day, and the way it was wrong is worth keeping. This section used to say K3 ships no preprocessor config, so the normalization was "the CLIP convention this lineage of towers uses rather than a value read out of the release" — an assumption, labelled as one. The release does ship the file; the downloader fetched a hardcoded list of filenames and never asked the repo what it contained. The tower still matched its oracle at 2.3e-06 throughout, because the oracle is fed random pixels and never touches the normalization. An honest caveat is not a substitute for reading the file. | build | model-free suite | backend | | |---|---|---|---| | macOS arm64 | yes | 23 pass / 0 fail / 11 skip | NEON | | Linux arm64 | yes | 23 pass / 0 fail / 11 skip | NEON | | Linux x86_64 | yes | 23 pass / 0 fail / 11 skip | AVX2 | | Windows x86_64 | yes | container, CLI and forward pass — see below | AVX2 | The first three run the same suite and now agree check for check: same 23 passes, same 11 skips, same list. CI has no container, so tests/run.sh builds a synthetic one and the checks that need real weights say SKIP rather than passing quietly. All three also pass the sanitizer suite and 400 fuzz cases. Windows is cross-compiled with MinGW-w64 on a Linux runner and then run on a Windows one: the binary reads a synthetic container, opens it from the CLI, and produces the same logits token-by-token as it does in chunks. It is not the same suite — tests/run.sh is a bash script that rebuilds first, and the Windows job runs binaries it did not build — so what is claimed is what that job checks and no more. Nobody has run it on a real container there. The platform is the variable, the suite is not, and that is the point: both Linux targets produce the same continuation as macOS and pass engine matches the PyTorch oracle when given a container, so the numerics carry across architectures and compilers. SIMD is selected at run time from CPUID, so a single x86 binary uses AVX-512 where it exists and AVX2 where it does not. Accelerator backends are build-time options. A Metal backend exists and is off by default because it is correct and 22% slower: this engine issues several hundred small dependent matvecs per token, the worst possible shape for an accelerator, and the CPU path already runs at the machine's memory bandwidth. src/ the engine — 6,000 lines of C, no dependencies model.c forward pass, MoE routing, KDA and MLA layers ecache.c bounded LFRU expert cache over the banks vision.c the 27-layer ViT and the projector into text space image.c a file on disk to the patch tensor the tower wants waste.c the public API simd_*.c per-ISA kernels, selected at run time cli/ the CLI, a client of the public API serve/ the OpenAI-compatible server, the other client xtml.py K3's prompt format, ported from the release's encoding_k3.py regions.py its replies, back into reasoning / answer / tool calls engine.py libwaste through ctypes, and the request queue server.py /v1/chat/completions and friends tools/ conversion and validation (Python, never at run time) docs/ format, engine, backends, and what was learned tests/ 34 checks, and a diff against a PyTorch oracle given a model serve/ 149 more for the server, incl. a differential vs upstream examples/ chat.json for K3 and ChatML, the format a container carries third_party/ stb_image.h, the single vendored header — see its README docs/LEARNED.md is the one to read before contributing. It records what was measured, including the optimizations that were refuted — index-layout blocking, a 3-bit trunk, GPU offload, per-expert bit allocation — with the numbers that killed them. The API is not frozen, as above. The rest is stated plainly too, because finding these out for yourself is worse than reading them here: - a container carries its chat format in chat.json , and the converter can only fill it in for a model whose format has been transcribed from its reference encoder — K3 today. Neither Kimi release distributes a template, so for anything else the CLI says so and continues raw rather than guessing a format, which would produce plausible wrong answers instead of visibly wrong ones. Kimi-Linear is in that position now; - AVX-512 compiles and is dispatched from CPUID, and has still never executed an instruction. This laptop is ARM and its x86 emulation is Rosetta, which reports AVX2 and leaves the ZMM state disabled in XCR0; the hosted x86 runner is an AMD EPYC 7763, which answers avx512f/bw/dq/vl: no , so CI says AVX2 as well — on Linux and on Windows both. The workflow prints the runner's flags before every build, so the day a runner has them the SIMD backend matches the CPU baseline check becomes the confirmation without anyone arranging it; - Windows builds and runs, on one toolchain and one CPU. MinGW-w64 x86_64, cross-compiled, with src/platform.h holding the six calls that are not POSIX: the positional read, the aligned allocation, the CPU count, the file size andFILE_FLAG_NO_BUFFERING for the cache-bypass open. MSVC is a different port and has not been attempted — the sources use GNU C. ARM64 Windows is not built. Neither is the page-cache bypass proven under load there: CI confirms Windows grants it on the runner's filesystem, which is not the same as measuring a hit rate against a container that does not fit in RAM; - the expert checksum is off unless you ask for it ( --verify ), and the trunk has no checksum at all. The first is a decision — 5% of throughput on every token, against a container that is usually fine — and it means the default build of a rotted container still answers with whatever the damaged bytes decode to. Run--verify once after copying a container, to establish that it arrived intact.tools/verify_container.py does not stand in for that: it re-derives records against the source weights, so it wants torch and the original checkpoint on disk, and it answers whether the conversion was right rather than whether the copy still is. The second is not a decision: the trunk and the codebooks have nothing to check against in the format, and nothing has been built in its place; - every expert in a container is at the same bit width. The non-uniform per-expert allocation the format was designed around is not coming: it was measured on both models rather than built, and the importance it would allocate against does not vary — the value of the third bit spreads at most 1.15x between experts in a layer and 1.01x between layers, so the optimal allocator and a coin flip write the same container. The one signal that is not flat, routing frequency, buys disk footprint and almost no I/O, which is the resource that is actually scarce. docs/LEARNED.md §20 has the table and the one measurement that would revive it. Apache 2.0 — see LICENSE. Copyright 2026 SQLite Cloud, Inc.

2

Attention Decode on AMD MI450 GPUs: A Gluon Kernel Optimization Guide

Hacker News · original → · 8/10 · Work/tech: LLM inference optimization on AMD GPUs
Attention Decode on AMD MI450 GPUs: A Gluon Kernel Optimization Guide# Agentic AI applications are pushing LLM inference into a new regime. A request can now reach one million tokens from aggregated…

Attention Decode on AMD MI450 GPUs: A Gluon Kernel Optimization Guide# Agentic AI applications are pushing LLM inference into a new regime. A request can now reach one million tokens from aggregated long prompts, tool calls, retrieval results, and multi-turn reasoning. During the text generation phase, each new token must attend to all previous tokens from the KV cache, so the kernel needs to repeatedly read past states from HBM. As a result, the performance bottleneck shifts from compute units to the memory system. The AMD Instinct MI450 series GPU introduces a new set of hardware features for memory-sensitive AI workloads. This blog uses attention decode as a case study and discusses how to design a high-performance kernel on MI450. We will walk through core hardware features, explain how kernels can use these features, and showcase the performance of an optimized Gluon kernel, which achieves 85% of the peak HBM bandwidth on MI450 as an early result. MI450 Hardware Overview# MI450 is a large step forward from the MI350 series for AI workloads. Compared with previous generations, MI450 provides more on-chip resources and larger HBM capacity with higher bandwidth. The following table summarizes the key spec changes between MI350 and MI450. A Workgroup Processor (WGP) is the hardware unit that runs a workgroup (named CU in earlier MI-series). In this model, one WGP contains four SIMD32 units, each with 32 lanes. Each SIMD32 has its own Vector General-Purpose Registers (VGPRs), which are used by the Vector ALU unit (VALU) and Wave Matrix Multiply-Accumulate unit (WMMA). All SIMD32 units in a WGP share the same Local Data Share (LDS) memory. Specification | MI350 series | MI450 series | |---|---|---| VGPRs per SIMD | 512 | 1024 | Max LDS per WGP | 160 KB | 320 KB | HBM capacity | 288 GB | 432 GB | HBM bandwidth | 8.1 TB/s | 19.6 TB/s | In addition to more on-chip resources, MI450 introduces a specialized hardware unit - TDM, to help move structured tensor data between global memory and LDS. With TDM, the kernel describes the target tensor to access using its memory address, shape, strides, and layout, then issues a bulk data transfer asynchronously. TDM helps accelerate memory access and also makes it easier to write pipelined kernels where compute and memory can be overlapped. This marks a significant change from MI350, where the kernel must issue many small vector loads to move data from global memory to LDS. Features | MI350 series | MI450 series | |---|---|---| Memory instruction | Global/Buffer load to LDS | TDM load to LDS | Load granularity | 32/96/128-bit vector loads | Descriptor-based tensor tiles | Memory units per WGP | 1 | 2 | Moreover, MI450 further extends the workgroup model with workgroup clusters. A normal workgroup runs on one WGP, independently of other workgroups. A cluster lets several workgroups, each running on its own WGP, coordinate through hardware-supported cluster barriers. Workgroups that access the same data can use multicast loads to share data across WGPs. This gives kernels a way to express cooperation at a larger scope, without falling back to a separate kernel launch or using global memory for synchronization. The figure below shows a kernel programmer’s view of the whole MI450 GPU hierarchy. To write the MI450 kernel more efficiently, we will use Gluon in this blog. Gluon is a Triton-based DSL that keeps the tile-based SPMD programming model. Unlike Triton, tensors in Gluon must carry an explicit layout, which describes how each element is distributed across registers, lanes, waves, and workgroups. The layout also affects how the compiler generates instructions to move data among different memory hierarchies. This makes Gluon a lower-level programming language than Triton, and especially useful for kernel experts who want explicit control over generated instructions. We assume the reader is familiar with the basics of Gluon semantics and its programming model. For Gluon kernel examples, please refer to the MI450 Gluon Examples. Attention Decode Basics# With this hardware context in place, we can now turn to the workload: attention decode. We will start with a baseline attention decode kernel, then use it as the reference point for the optimizations in later sections. The attention operation can be expressed as follows: first, we perform a matrix multiplication of Q and K, then apply softmax to get P, and multiply P with V to get the final output O. In this blog, we use general 4D tensors to represent the attention inputs and outputs. For notation throughout this blog, B is the batch size, H_q is the number of Q heads, H_kv is the number of KV heads, T_q is the number of Q tokens, T_kv is the number of KV tokens, and D is the head dimension. The input QKV tensors are shown below. Q: [B, H_q, T_q, D] K: [B, H_kv, T_kv, D] V: [B, H_kv, T_kv, D] When H_q is equal to H_kv , we have standard multi-head attention (MHA), and when H_q is a multiple of H_kv , we have multi-query attention (MQA) or grouped-query attention (GQA). Modern LLMs often use MQA, such as GPT-OSS, so we will focus on MQA in this blog. Attention in LLM inference includes two phases: prefill and decode. For prefill, T_q = T_kv , and the kernel can process many tokens in parallel. For decode, T_q = 1 , but T_kv remains large and grows with the context length. Given each head is independent, standard MHA can waste a lot of compute resources for single-token decode. However, in MQA decode, because multiple Q heads share one K and V head, the kernel can group those Q heads together. This can be reflected in the tensor shapes, as shown below, where the Q tensor is reshaped to group H_q / H_kv Q heads together for each KV head. Q: [B, H_kv, H_q / H_kv, D] K: [B, H_kv, T_kv, D] V: [B, H_kv, T_kv, D] Because tensors in each batch and each KV head are independent, we can process them in parallel. The kernel can be launched with a grid of (B, H_kv, 1) , and each program processes a portion of the attention computation: Q: [H_q / H_kv, D] K: [T_kv, D] V: [T_kv, D] Following the Flash Attention algorithm, the kernel can compute one tile of K and V with shape (BLOCK_N, D) and one Q tile with shape (BLOCK_M, D) at a time, then slides through the T_kv dimension. The figure below shows an example workload with 2 batches and 2 KV heads. Each workgroup owns one (B, H_kv) pair, so there are 4 workgroups in total. Within each workgroup, multiple Q heads are processed together and shown as multiple rows. Here, we assume BLOCK_M = H_q / H_kv . Decode Kernel Optimizations# So far, we have introduced a baseline implementation of an MQA decode kernel on MI450. This section will further discuss a series of optimizations that push the kernel toward peak performance on MI450. We will cover 4 major optimizations: tensor layout, data loading, pipelining, and parallelization with Split-k. Optimize Tensor Layout# Gluon gives kernel authors explicit control over tensor layouts, which matters a lot for performance. A kernel can contain many different tensors, and deciding the right layout for each of them is non-trivial. In practice, the most important layout to decide first is the layout for the WMMA operands. In an attention kernel, there are two WMMA operations: QK and PV. We can start our layout optimization there. In Gluon, AMDWMMALayout describes the WMMA output layout, and DotOperandLayout describes the WMMA operand layout. A given AMDWMMALayout determines the final WMMA instruction the compiler generates. For example, an AMDWMMALayout with instruction shape [16, 16, 128] for wmma_scaled generates v_wmma_scale_f32_16x16x128_f8f6f4 under the hood. This instruction consumes one FP8 operand of shape (16, 128) , another FP8 operand of shape (128, 16) , reduces over the K dimension of size 128, and produces a FP32 output tile of shape (16, 16) in one wave, where each element is assigned to a specific lane and register in this wave. The following figure shows how elements in the 2 WMMA operands and output tensor are assigned to lanes. Next, we need to consider how waves are distributed. The kernel can use any power-of-two number of waves and distribute them across 2 dimensions of the WMMA output tile. Since online softmax reduces along T_kv dimension, we prefer to distribute waves along grouped Q dimension to avoid cross-wave communication during the reduction. In MQA decode, the Q tile has shape (H_q / H_kv, D) , and H_q / H_kv is usually small, so we also need to avoid using too many waves there. For example, if H_q / H_kv = 32 , the kernel chooses 2 waves, as shown below. In this figure, 2 waves have their own first WMMA operand, and share the second operand. Another important factor to keep in mind is layout conversion. When two tensors in Gluon use different layouts, the kernel must use an explicit layout conversion operation to match them. Depending on the source and destination layouts, this conversion can happen in registers or through LDS. In attention, the output of the QK WMMA becomes the input of the PV WMMA after softmax. If the QK output layout is not compatible with the PV input layout, the hot loop pays an extra conversion cost. One useful technique is to “transpose” the WMMA output layout. This can be done by simply setting transpose=True in AMDWMMALayout , and the compiler will generate corresponding instruction to produce the transposed output. Note that this transpose operation does not incur extra instructions. For more details, please refer to this talk. Another detail worth mentioning is the concept of “K Width”. In DotOperandLayout , K Width describes how many contiguous elements along the K dimension for WMMA should be assigned to one lane. A standard 16x16x128 WMMA instruction assumes K Width 16, but the transposed output for QK can have K Width 8. Therefore, we also need to explicitly set the PV input layout with K Width of 8 to match the QK output. Optimize Data Loading# The hot loop of the decode kernel is dominated by loading K and V tiles from global memory to LDS. TDM helps accelerate this data path, but using TDM naively is not enough. To understand the performance impact of TDM, we first need to understand the cache hierarchy on MI450. MI450 has a per-WGP cache at the same level as the LDS. But different from LDS, this cache is not directly visible to the programmer. TDM can either move data directly into the programmer-managed LDS or route the data through the cache path before it reaches the destination. Considering the limited size of the cache, memory-bound workloads are better off bypassing the cache and moving data directly into LDS. Whether a TDM transfer uses the direct path depends on the shape of the TDM request. The innermost dimension needs to be at least 128 bytes to use the direct path, and 256 bytes is recommended to keep more in-flight memory traffic. Recall that K and V tiles have shape (BLOCK_N, D) , so the innermost dimension is the head dimension D . Assuming an FP8 data type, this means the head dimension needs to be 256 for the optimal performance. In practice, head dimension is determined by the model architecture and cannot be changed by the kernel. However the kernel can reshape the K and V tensors to increase the innermost dimension before loading, then reshape them back while loading from LDS to registers. The following figure shows an example data flow of K and V for head dimension of 128. Pipeline for Latency Hiding# So far, we have optimized the tensor layout and data path. However, the hot loop can still stall on memory access latency. The next step is to pipeline the loop so that memory movement for one tile overlaps with computation for another tile. To understand the pipeline, we can zoom into the attention formula shown earlier and write it in the tiled form used by the Flash Attention algorithm. The formula below shows one iteration i of the loop that processes one KV tile. Here we only focus on operations on 2D tiles, and omit operations on reduced 1D intermediate values. We use short operation names on the right side to denote each operation, making the pipeline easier to discuss later. Each line in the figure can be expressed as one Gluon operation, which will be expanded to a sequence of instructions. The following table shows the expanded instruction groups for FP8 decode attention with BLOCK_M = 32 , BLOCK_N = 128 , D = 128 , and two waves. The Cycles column gives a simplified per-instruction cost estimate based on hardware specifications. Memory instructions are left blank because their latency is modeled separately. Operation | Instruction | Count | Cycles | |---|---|---|---| TDM K | | 1 | | LDS K | | 32 | | QK | | 8 | 8 | MAX | | 32 | 1 | FMA | | 32 | 1 | EXP | | 64 | 2 | SUM | | 32 | 1 | MUL | | 32 | 1 | CVT | | 8 | 4 | TDM V | | 1 | | LDS V | | 64 | | PV | | 8 | 8 | This table gives us the basic scheduling units. The goal of pipelining is to move independent units from different loop iterations into the same time window. Our first attempt is to separate memory movement from computation. In the simple two-stage pipeline below, one stage issues TDM loads for future iterations, while the other stage consumes data that has already arrived in LDS. The notation [i] means the operation belongs to loop iteration i . This schedule needs double buffering. While one LDS buffer is being consumed by LDS loads and WMMA, the other buffer can be filled by TDM for a later iteration. On the next loop step, the two buffers swap roles. The two-stage pipeline can overlap TDM loads with computation. This works well when the loop is clearly memory-bound, but it treats the entire compute side as one monolithic block. As a result, LDS loads, WMMA for QK, WMMA for PV, a series of vector instructions for softmax are effectively serialized inside the compute stage. This unnecessarily inflates the compute block. In the worst case, the kernel can shift from being limited by TDM latency to being limited by serialized compute. To avoid this, we split the compute block according to the actual data dependencies. These operations do not all depend on the same values or use the same hardware resources, so WMMA instructions, LDS movement, and softmax vector work can often be interleaved once their operands are ready. In the dependency graph below, each node is one scheduling unit. An edge means the destination cannot start until the source has produced its value. We can observe that the QK for iteration i + 1 does not need the softmax result from iteration i , so the next KV tile can be prefetched and its QK computation can start earlier. Moreover, QK and PV use WMMA instructions, while the online softmax only uses VALU instructions, so these operations can be interleaved instead of serialized. These observations motivate the four-stage pipeline. Instead of putting all non-TDM work into one compute stage, the four-stage pipeline interleaves memory and compute units from neighboring iterations. A stage may contain QK from iteration i + 1 , softmax work from iteration i , an LDS load for iteration i , and a TDM request for a future iteration. We group the softmax work into two stages with roughly equal amount of work, to help hardware better interleave WMMA and VALU. Here, QK WMMA can interleave with VEC0 and PV WMMA can interleave with VEC1. In addition, EXP uses the transcendental unit and can be further interleaved with other VALU instructions. This four-stage schedule exposes more overlap than the two-stage schedule, but it still leaves one question: how far ahead should the TDM requests be issued? With double buffering, the kernel has 2 LDS slots. One slot is being consumed by LDS loads and compute, while the other slot is being filled by TDM. This gives each TDM request roughly one iteration of compute time before the data is needed. We can estimate whether that is enough with a simple cycle model. Assume TDM load takes about 1000 cycles and all LDS load latency can be hidden by scheduling. Using this model and the previous table, we can get the following estimate for one decode iteration shown in the table below. Here we use 2 interleave rules: 1) one WMMA takes 8 cycles, and can hide 2 cycles of VALU; 2) one EXP takes 2 cycles, and can hide 1 cycle of non-EXP VALU. Also note this 1000 cycles is an empirical number to help us reason about the pipeline. The actual TDM latency depends on many factors, like the tensor tile shape and access pattern. Group | Total Cycles | |---|---| QK | 64 | PV | 64 | VEC0 = MAX + FMA + EXP | 192 | VEC1 = SUM + MUL + CVT | 96 | QK + VEC0, interleaved | 176 | PV + VEC1, interleaved | 144 | Total, interleaved | 320 | In a double-buffered schedule, a TDM request has roughly 320 cycles of effective compute work to overlap with before the loaded tile is consumed. This is much smaller than the 1000-cycle TDM latency in this model, leaving about 680 cycles of exposed wait for each load. This motivates a longer lookahead distance. Triple buffering adds one more LDS slot, so the kernel can request a future tile earlier instead of waiting for the next buffer swap. Conceptually, one buffer is being consumed, one buffer is ready or close to ready, and one buffer is being filled by TDM. In the same model, looking ahead by two iterations gives about 640 cycles of effective compute work to overlap. It still does not cover the full memory latency, but it reduces the exposed wait from about 680 cycles to about 360 cycles and gives the hardware more outstanding memory work. Parallelize with Split-k# The pipeline optimizes latency hiding inside one workgroup, but one GPU has many workgroups, and we also need to make sure we can saturate the full GPU memory bandwidth across all workgroups. In the baseline MQA/GQA decode mapping, each workgroup owns one (B, H_kv) pair and walks through that pair’s KV sequence serially. The total number of workgroups is only B * H_kv . On MI450, there are 256 workgroup processors, so small batch sizes or small numbers of KV heads may launch too few workgroups to fully utilize the available memory bandwidth. Split-k addresses this under-utilization problem by partitioning the KV sequence across multiple workgroups. Instead of assigning the whole KV sequence for one (B, H_kv) pair to a single workgroup, Split-k divides it into S partitions and lets S workgroups process those partitions in parallel. The number of workgroups increases from B * H_kv to B * H_kv * S , improving GPU saturation while also reducing the number of KV blocks handled by each workgroup. The partial results from the partitions are then merged to produce the final attention output. The following figure shows an example with 2 batches, 2 KV heads, and 2 partitions: the total number of workgroups doubles, and each workgroup processes half of the KV sequence. One challenge with Split-k is that all workgroups also need to synchronize and merge their partial results. Typically, there is a separate reduction kernel to merge the partial results. This two-kernel approach uses global memory to store the intermediate results and also sets a natural synchronization point. MI450 introduces the new workgroup cluster feature, which allows a cluster of workgroups to synchronize. We can use this feature to implement Split-k in a single kernel by fusing the reduction compute. In Gluon, the concept of a workgroup cluster is expressed via multi-CTA programming. “CTA” is the Triton term for a workgroup, and “CGA” is the term for a cluster. The following table shows the mapping between Triton and MI450 concepts: Triton Concept | MI450 Concept | |---|---| Warp | Wavefront | CTA | Workgroup | CGA | Workgroup Cluster | Gluon kernels distribute CTAs just like warps; both are part of the layout system. One detail to note is that when discussing block size in Triton, it usually means the block size of one workgroup. With a multi-CTA layout, the block size is the size of the whole cluster. Layouts in Gluon, such as AMDWMMALayout , allow specifying the cga_layout field for the multi-CTA kernel. In the Split-k case, since each workgroup processes a partition independently until the final reduction, we can perform the partial attention in 3D format. Effectively, each program processes the attention computation shown below. All partitions here share the same Q. Q: [1, H_q / H_kv, D] K: [S, T_kv / S, D] V: [S, T_kv / S, D] To see how this maps to the layout system, it is useful to look at a single WMMA operation under a multi-CTA layout. In the figure below, we add one extra dimension to the WMMA layout for workgroups. The first operand is shared across 2 partitions. The second operand and output are different. Within each workgroup, the two waves are still distributed as before. After the partial attention, we need to merge the results from all workgroups. The kernel will first write the partial results to global memory, synchronize all workgroups in the cluster, and finally read back the partial results. It is also worth mentioning that MI450 provides the L2 cache which is shared across workgroups in a cluster. So the partial results do not need to be passed through global memory. This helps to cut the overhead of the reduction. As the reduction is fused into the same kernel, we will use the same launch grid for the reduction, but with a different layout. Workgroups are now distributed along the head dimension and loop through each partition to compute the final result. Performance Evaluation# So far, we have covered the main optimization steps for MQA decode on MI450, including: Optimize tensor layout: Choose optimal Gluon layouts to help the compiler generate better instructions; Optimize data loading: Optimize K/V TDM requests to speed up memory access; Pipeline for latency hiding: Overlap memory access, matrix multiplication, and softmax to reduce exposed memory latency; Parallelize with Split-k: Split the KV sequence across multiple workgroups to fully utilize the GPU memory bandwidth. Apart from all the optimizations discussed above, the kernel also uses a few other techniques to improve performance, including better code generation and underlying instruction-level optimizations in LLVM. We implemented the above optimizations in a Gluon kernel. The kernel source code is fully open source in the MI450 Gluon MXFP Attention Example. Our evaluation of this kernel covers the following target settings: Batch: 64 Number of Q heads: 64 Number of KV heads: 1 or 2 KV sequence length: 4096-65536 QKV data type: FP8 with a global scale We use the effective bandwidth of the kernel as the target metric, defined as the total number of bytes read and written to global memory divided by the total kernel execution time. For a system-level memory-bandwidth reference, we also measured the peak read-only bandwidth on the same MI450 system using the BabelStream. The following figure shows the performance of the kernel for the target settings: The reported peak bandwidth on our evaluation system is 20TB/s. In the GQA case with H_kv = 2 , effective bandwidth reaches 17.10 TB/s, 85% of the peak bandwidth. In the MQA case with H_kv = 1 , the kernel reaches 16.65 TB/s, 83% of the peak bandwidth. The throughput of the kernel increases with sequence length. At shorter sequence lengths, fixed costs such as prologue and reduction overhead take a larger fraction of the total runtime. The above results were collected with ROCm 7.14.0 and PyTorch 2.11.0, using Triton commit ecfc626 with the latest mxfp_fa_gfx1250.py script. To reproduce the results, please run the following command: # H_kv = 2 python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 4096 --num_q_heads 64 --num_k_heads 2 --head_sz 128 --pipelined --scale_type global --profile python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 8192 --num_q_heads 64 --num_k_heads 2 --head_sz 128 --pipelined --scale_type global --profile python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 16384 --num_q_heads 64 --num_k_heads 2 --head_sz 128 --pipelined --scale_type global --profile python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 32768 --num_q_heads 64 --num_k_heads 2 --head_sz 128 --pipelined --scale_type global --profile # H_kv = 1 python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 8192 --num_q_heads 64 --num_k_heads 1 --head_sz 128 --pipelined --scale_type global --profile python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 16384 --num_q_heads 64 --num_k_heads 1 --head_sz 128 --pipelined --scale_type global --profile python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 32768 --num_q_heads 64 --num_k_heads 1 --head_sz 128 --pipelined --scale_type global --profile python3 third_party/amd/python/examples/gluon/mxfp_fa_gfx1250.py --q_type e4m3 --kv_type e4m3 --batch 64 --seqlen_q 1 --seqlen_k 65536 --num_q_heads 64 --num_k_heads 1 --head_sz 128 --pipelined --scale_type global --profile Summary# In this blog, we used attention decode as a case study to walk through the main optimization steps for MI450. We started from a baseline attention loop, then optimized the WMMA layouts, data loading, and software pipelining. We also discussed how to use split-k with workgroup clusters to increase parallelism for long-context decode. The optimized attention decode kernel can reach 85% of the peak memory bandwidth on MI450. This is an early result, and we expect to further improve the kernel performance with more optimizations in the future, such as memory prefetching, better instruction-level scheduling and improved reduction for split-k. We also demonstrated that MI450 provides several hardware features that are especially useful for decode workloads: TDM for asynchronous global-to-LDS movement, larger register and LDS resources, and workgroup clusters for cooperation across workgroups. Gluon exposes all of these features and allows kernel authors to perform low-level optimizations while maintaining a tile-based SPMD programming model consistent with Triton. This opens up many optimization opportunities for kernel experts to achieve peak performance on MI450. Moving forward, we plan to continue to apply these optimization techniques to other attention decode kernels, covering different data types, paged KV cache, and also different attention variants like MLA/DSA kernels in the DeepSeek family. These kernels will be further pushed to production-ready quality and integrated into LLM inference frameworks. Disclaimers# The information presented in this document is for informational purposes only and may contain technical inaccuracies, omissions, and typographical errors. The information contained herein is subject to change and may be rendered inaccurate for many reasons, including but not limited to product and roadmap changes, component and motherboard version changes, new model and/or product releases, product differences between differing manufacturers, software changes, BIOS flashes, firmware upgrades, or the like. Any computer system has risks of security vulnerabilities that cannot be completely prevented or mitigated. AMD assumes no obligation to update or otherwise correct or revise this information. However, AMD reserves the right to revise this information and to make changes from time to time to the content hereof without obligation of AMD to notify any person of such revisions or changes. THIS INFORMATION IS PROVIDED ‘AS IS.” AMD MAKES NO REPRESENTATIONS OR WARRANTIES WITH RESPECT TO THE CONTENTS HEREOF AND ASSUMES NO RESPONSIBILITY FOR ANY INACCURACIES, ERRORS, OR OMISSIONS THAT MAY APPEAR IN THIS INFORMATION. AMD SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR ANY PARTICULAR PURPOSE. IN NO EVENT WILL AMD BE LIABLE TO ANY PERSON FOR ANY RELIANCE, DIRECT, INDIRECT, SPECIAL, OR OTHER CONSEQUENTIAL DAMAGES ARISING FROM THE USE OF ANY INFORMATION CONTAINED HEREIN, EVEN IF AMD IS EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. AMD, the AMD Arrow logo, and combinations thereof are trademarks of Advanced Micro Devices, Inc. Other product names used in this publication are for identification purposes only and may be trademarks of their respective companies. © 2026 Advanced Micro Devices, Inc. All rights reserved Cautionary Statement# This blog may contain forward-looking statements concerning Advanced Micro Devices, Inc. (AMD), which are made pursuant to the Safe Harbor provisions of the Private Securities Litigation Reform Act of 1995. Forward-looking statements are commonly identified by words such as “would,” “may,” “expects,” “believes,” “plans,” “intends,” “projects” and other terms with similar meaning. Investors are cautioned that any forward-looking statements in this blog are based on current beliefs, assumptions and expectations, speak only as of the date of this blog and involve risks and uncertainties that could cause actual results to differ materially from current expectations. Such statements are subject to certain known and unknown risks and uncertainties, many of which are difficult to predict and generally beyond AMD’s control, that could cause actual results and other future events to differ materially from those expressed in, or implied or projected by, the forward-looking information and statements. Investors are urged to review in detail the risks and uncertainties in AMD’s Securities and Exchange Commission filings, including but not limited to AMD’s most recent reports on Forms 10-K and 10-Q. AMD does not assume, and hereby disclaims, any obligation to update forward-looking statements made in this blog, except as may be required by law.

3

Tailscale didn't stop the Hugging Face intrusion

Hacker News · original → · 8/10 · AI/work: AI agent sandbox escape and security implications
An AI agent escaped its sandbox, entered Hugging Face’s infrastructure, and used a stolen Tailscale credential to enroll 181 nodes onto their tailnet. No Tailscale vulnerability was found or…

An AI agent escaped its sandbox, entered Hugging Face’s infrastructure, and used a stolen Tailscale credential to enroll 181 nodes onto their tailnet. No Tailscale vulnerability was found or exploited—we should have been able to prevent it anyway. Tailscale didn’t stop the Hugging Face intrusion By now, you’ve likely heard about the AI agent that escaped a security evaluation and attacked Hugging Face, an LLM marketplace. The agent decided Hugging Face might have the answers to its benchmark, so it stole them just to cheat on the exam. That's a funny motive but a scary outcome. Hugging Face published a detailed reconstruction of the intrusion. It covers about 17,600 recovered actions over four and a half days, including sandbox escapes, code execution, cloud credentials, improvised command-and-control systems, and eventually, the use of Tailscale to spread throughout their organization. But Tailscale is a zero trust network! The whole point of zero trust is to prevent attackers from moving laterally across your company. What went wrong? Where Tailscale appeared No “vulnerabilities” in Tailscale were found or exploited, and that might make it even more uncomfortable for us. Tailscale turns up everywhere in AI infrastructure: virtually every AI company uses Tailscale. So it's not surprising we were in an AI incident report eventually. But, we're a security tool. Their intrusion is our intrusion, and it's our job to take it seriously. As in any post-mortem, we need to reflect on what happened and what we could have done to prevent it. By the time the agent escaped its sandbox and found Tailscale, it had already gained code execution privileges inside a production worker, reached root access on a Kubernetes node, and read a production secret store containing 136 keys. In a sense, it was game over before we even arrived on the scene. But isn't that the problem? Let's back up a level or two. Why were long-lived secret keys accessible at all to anyone? Because long-lived credentials are the standard They shouldn't be, though. And we've now seen a great example of why. In the old world where most intrusions were done by humans at human speed, credential leak mitigations were treated as a nice-to-have. A big credential store, where you can read 136 keys at once, was a to-do item somewhere in a security team's low-priority list. Now, in a world of rogue AI agents, the big credential vault is the prize. It's not okay anymore. I only know of two main solutions to long-lived credentials. First, you can configure a vault that only issues short-lived creds based on long-lived creds that you insert once and that it never gives back. HashiCorp Vault can do this, for example; they call it dynamic credentials. Unfortunately, dynamic credentials are a lot of work to set up and maintain. When security requires work, people don't do it. The second option is a credential-injecting proxy. If client C wants to access server S, don't give C credentials for S; instead, have it send its requests via a hardened proxy P that does nothing but receive a request, insert a credential, and pass it through. Credential injection is gaining popularity now that modern mesh-based networking removes most of its latency. A few months ago, we acquired Border0 ("Tailscale PAM"), which makes an elegant and quite fancy credential-injecting proxy, tightly integrated with Tailscale. Border0, fully deployed, would have prevented reading those 136 keys and logged every attempt to use one. (The same credential injection powers Aperture Connectors.) But, this stuff is too recent. Most of our customers haven't even looked at it yet. A bonus third option: Tailscale itself. In Tailscale, your node key can be machine bound using a TPM to prevent export, and if you're an enterprise you should definitely do that. Alas, we had to turn TPM storage off by default on Linux and Windows, because of HSM issues we found on some users' hardware. Anyway, in today's case, the attack is what granted access to Tailscale, so we arrived too late. That long-lived auth key didn’t need to exist Neither dynamic credentials nor credential-injecting proxies were in place, so 136 keys leaked. What's the next line of defense? One of those 136 credentials was a reusable Tailscale auth key, used to create new Tailscale CI (continuous integration, used for automated testing) nodes in their tailnet. The agent copied that key into a series of external sandboxes and used it, over several days, to enroll a total of 181 nodes into Hugging Face’s tailnet. Those nodes each received a Tailscale identity tag granting all the access a CI node would get. We couldn't save those 136 keys. But a reusable Tailscale key didn’t need to be among them. We built workload identity federation for cases like this. It relies on your cloud provider to offer "ambient authority" in the form of short-lived creds generated from a running VM's (or container's) cloud identity. The CI job can ask its cloud platform for a signed OIDC token. Tailscale verifies the token, then grants access with the tags and scopes assigned to that workload. What's neat is this can happen automatically once enabled: spin up the CI node, Tailscale gets the identity, assigns the right tags. There's no credential to leak and, when configured carefully, no way to use this credential anywhere but in CI. That means you can't take a CI credential, run it on a node elsewhere, and turn it into a paved route out of the test cluster into the rest of the network. Alas, not enough people use workload identity federation. We should have done more to make sure customers knew it's the easier and safer path and had a clear way to adopt it. We can improve the docs, and add more nudges in the UI. No client logs doesn’t mean no evidence In an attempt to hide its tracks, the agent ran Tailscale with --no-logs-no-support , which suppresses reporting from that client. That's an option designed for users who are concerned about sending telemetry metadata to Tailscale. Even if we didn't offer it, it would be easy to modify the source code to remove the telemetry. But stopping the logs doesn’t make the connection invisible. If you enable Tailscale network flow logs, they report traffic from both ends of every connection, as well as from subnet routers and exit nodes. This is subtle but important: a compromised node might not send flow logs, but every node it connects to does. And then your SIEM, configured with care, can raise an immediate red alert if the two ends don't match. Flow logs can help detection when they stream into a carefully configured SIEM. But that's a lot of work. Flow logs need to be enabled, and you need to have the right live detection rules in place so they’re useful in real time, not just for forensics later. We’re looking at how to make flow logs easier to discover, configure, adopt, and serve as alert triggers. I want us to make flow logs so easy to use that they help even if you don't have a security team to watch them. If you want direct control beyond just logging, you can also enable Tailnet Lock. This gives you direct visibility and strict, programmable admission control for every single new node. For example, with some work, you could program your signing node to check that "CI" tags always have a particular IP address range or other side-channel proof of validity. Make the safe path the easy path Network security is hard. It has always been hard. In the new world of rogue AI agents, it's not just hard, but essential. And that's a problem because many orgs simply don't have network security expertise. So at Tailscale, we take it personally. People expect our product to prevent these sorts of lateral movement attacks, by default, so they don't have to. Even if they have no idea what a lateral movement attack is. If this incident has you looking a little nervously at your own infrastructure, start by looking at the reusable Tailscale auth keys your workloads can read. For cloud and CI in particular, replace them with workload identity federation wherever you can. Get rid of those long-lived auth keys. (Auth keys still have good uses, especially for one-time provisioning and environments without a platform identity. When you need one, prefer one-off keys; use OAuth clients to keep the auth key expiry periods short; use narrow tags; audit the permissions granted to those keys in your ACLs.) Turn on network flow logs and send them to the tools your security team already uses. Use secure node state storage on managed fleets, where you have control over your TPMs. Use device posture to isolate and restrict nodes where you don't. I know we haven’t made these safer choices obvious enough. That’s on us. We'll improve our docs, add nudges to the UI, do our best to turn these on by default, warn you when you're doing something dangerous, and suggest better alternatives. This is our very Canadian apology: sorry you stepped on our toes. The attack didn’t exploit Tailscale, and Tailscale didn’t cause the compromise. But, we didn't stop it. Next time, we will. If you run Tailscale and want to dig deeper, get in touch with our support and solutions engineering teams. We can help you harden your settings and help you find the rough edges before the next AI agent does. Author

4

deepseek-ai/DeepSeek-V4-Flash-0731

Simon Willison · original → · 8/10 · AI: DeepSeek V4 Flash model release and value analysis
31st July 2026 - Link Blog deepseek-ai/DeepSeek-V4-Flash-0731 (via) The latest release in DeepSeek's V4 family, "with substantially enhanced agentic capabilities". It's 304 billion parameters -…

31st July 2026 - Link Blog deepseek-ai/DeepSeek-V4-Flash-0731 (via) The latest release in DeepSeek's V4 family, "with substantially enhanced agentic capabilities". It's 304 billion parameters - 167GB on Hugging Face - but it appears to punch well above its weight. Artificial Analysis rank it ahead of MiniMax M3 - a 428B model. It's $0.14/million input and $0.27/million output pricing means this may currently be the best value-per-intelligence model out there. It's looking very good on the Intelligence Index vs. Cost per Intelligence Index Task chart: I got a disappointing pelican from it using the default reasoning level via OpenRouter: But when I bumped reasoning level up to high I got something much better: llm -m openrouter/deepseek/deepseek-v4-flash-0731 -t pelican -o reasoning_effort high

5

Quoting Akshat Bubna

Simon Willison · original → · 8/10 · AI/work: rogue AI agent sandbox escape incident analysis
28th July 2026 We’re aware a Modal customer published an unauthenticated endpoint that allowed anyone on the internet to use their sandboxes for code execution. This was used by the rogue agent.…

28th July 2026 We’re aware a Modal customer published an unauthenticated endpoint that allowed anyone on the internet to use their sandboxes for code execution. This was used by the rogue agent. Modal’s platform or isolation were not compromised in anyway. — Akshat Bubna, Modal's CTO, talking to Reuters about this incident

6

Bree Macra reflect on first 65 years

Wexford Local · original → · 7/10 · Local Wexford: Bree Macra community organization anniversary
[image →]Pictured at Bree Macra na Feirme’s recent annual general meeting were (Back Row: Eddie Casey, Anthony Doyle, Keran Banville, David Finn, Gary Murphy, Barry Murphy. (Front Row); Grace Kehoe,…
[image →]
Pictured at Bree Macra na Feirme’s recent annual general meeting were (Back Row: Eddie Casey, Anthony Doyle, Keran Banville, David Finn, Gary Murphy, Barry Murphy. (Front Row); Grace Kehoe, Sinead Kinsella, Katie Sutton, Eimear Jackman, Sarah Byrne, Mary Byrne, Brónagh Murphy.

By Dan Walsh

The annual general meeting of Bree Macra Na Feirme held in Bree Community Centre featured opening addresses from Chairperson Eddie Casey and Secretary Mary Byrne reflected on the club’s many high points throughout their 65th year.

They thanked the outgoing committee for their dedication throughout the year, and expressed sincere gratitude to former members Willie Wickham, Kate English, Brendan Byrne, and Sinéad Kinsella (née Doyle), who worked with current club members in organising the 65th Anniversary Dinner Dance in September.

The event saw 150 past, present, and future members of Bree Macra, as well as well-wishers from further afield, gather to celebrate 65 years of the club’s success. Bree Mara stalwart PJ Darcy was honoured with a presentation by the 60th Anniversary Committee of Niall Doyle, James Byrne, Sinead Doyle (née Kinsella), Áine Doyle, Cáit Doyle, and Johanna Wickham, in recognition of his decades of service.

The annual general meeting also saw James Byrne and PJ Darcy thanked for their continued dedication to coaching the club’s Public Speakers and Debaters. Both highly decorated alumni in their own right, they have been essential to the club’s continued success in competitions, most recently in bringing home All-Ireland Titles in Novice Debating, with a team of Sarah Byrne, Eddie Casey, and Mark Waters, and Team Public Speaking, with a team of Mary Byrne, Sarah Byrne, and Eddie Casey.

The club also took home the All-Ireland title in Mastermind (Sarah Byrne), third place in Question Time (Sarah Byrne, Mary Byrne, Eddie Casey, and Sinead Kinsella), and joint third place in Creative Writing (Sinead Kinsella). They were well represented in a number of other national competitions, including in the Mr. Personality Festival by Kieran Banville.

Bree Macra has also participated in a growing number of community events and charitable causes, such as the Adamstown Agricultural Show, the Shoebox Appeal and Relay for Life. The club has grown exponentially over the last number of years, with a current active membership of almost thirty.

The 2026 AGM reflected this growth, with a total of ten officers elected, including three newly formed positions. The new committee consists of Chairperson Eddie Casey, Secretary Mary Byrne, Treasurer Katie Sutton, PRO Brónagh Murphy, Print PRO Sarah Byrne, Competitions Officer Anthony Doyle, Development Officer Eimear Jackman, Agricultural Affairs Officer David Finn, Rural Youth Officer Gary Murphy, and Ordinary Committee Member Kieran Banville.

The club already has a variety of exciting activities planned, with Beach Rounders taking place on Curracloe Beach at 8pm on the first, second and fourth Friday of the month. Newcomers are always welcome and can contact club Chairperson Eddie Casey on 087 1007373.

7

The Abrupt Fall of Situational Awareness Is a Warning Sign. So Is Nvidia’s Vendor Financing.

Newcomer · original → · 7/10 · AI: AI hedge fund market warning and industry leverage
The Week in ShortLeopold Aschenbrenner’s AI-centric hedge fund sells its public holdings after a market rout, a warning flare for the increasingly leveraged AI industry. TBPN Host John Coogan talks…
The Week in Short

Leopold Aschenbrenner’s AI-centric hedge fund sells its public holdings after a market rout, a warning flare for the increasingly leveraged AI industry. TBPN Host John Coogan talks tech media and life after OpenAI ownership on the podcast. SPVs outpace direct share listings as secondary market mechanisms. A Thinking Machines Lab co-founder steps down citing health issues, but quickly takes a job at OpenAI. The Trump administration bans Chinese robots. Google DeepMind’s AlphaFold team sees a personnel shakeup and a few notable departures. Live-shopping app Whatnot offers a sign of life for consumer dealmaking.


The Main Item

As Markets Wobble, Circular Financing & Leveraged Bets Ramp up the Risks

The rapid retreat of AI and chip stocks over the past couple of weeks has exposed the fragility of an AI funding boom that’s drawing capital from every corner of the financial system — and is increasingly reliant on debt.

Situational Awareness, the much-touted San Francisco hedge fund led by 25-year-old wunderkind Leopold Aschenbrenner, this week provided an object lesson in the risks. Less than two months ago, the former OpenAI researcher was being celebrated by the Wall Street Journal for his prescience in seeing the AI opportunity early and turning a few hundred million dollars into well over $20 billion in less than two years. (The firm’s net asset value reportedly reached $45 billion at its height.)

On Thursday, he was forced to sell the fund’s public stock portfolio to Citadel after margin calls had left it in a liquidity crisis.

The accelerationist-leaning pseudo-anonymous account @beffjezos called the incident a “huge hit for EA-adjacent rationalist former big lab alignment researcher kind” and many meme-d Citadel’s Ken Griffin into the grim reaper of speculative investing.

It’s not that Aschenbrenner’s bets were wrong, exactly: the infrastructure and chip plays at the heart of the portfolio, including names like SanDisk, SK Hynix and CoreWeave, are still trading far above where they were a year ago. He himself has always spoken of the AI build-out as a long-term opportunity.

But “conviction” in this case meant levering up the wagers with borrowed money, and thus declines on the order of 30% on core holdings created a sudden cash crisis. It’s a reminder of how quickly fortunes can turn in an AI trade where valuations — and thus expectations — remain sky high, and risk is often stacked upon risk.

A similar red flag in that regard comes in the form of the latest wave of multi-billion-dollar Nvidia investment deals. This week, it was a $5 billion commitment to Ilya Sutskever’s Safe Superintelligence, much of which will be used to buy Nvidia gear.

Nvidia is reported to be discussing a loan guarantee for OpenAI of as much as $250 billion, also tied to Nvidia hardware.

That’s on top of $90 billion in corporate VC investments over the last 16 months alone, per the Financial Times. Nvidia participated in 283 funding rounds between 2021 and 2025, with 85% of those investments in AI startups, according to Crunchbase.

The Bank of Jensen

In many cases, these investments involve Nvidia essentially providing the money for customers to buy its hardware. Indeed, CEO Jensen Huang has been so aggressive in funding the AI ecosystem that the company is functioning almost like a bank; with annual free cash flow nearing $100 billion and little debt, that isn’t obviously reckless.

Yet such vendor financing, though it has plenty of precedents, is what brought down Lucent and much of the rest of the telecom equipment industry in the early 2000s after the first dot-com bubble burst.

Nvidia’s defenders point out that many of its deals are collateralized by the hardware, so if the company fails Nvidia gets the gear back. In a bust scenario, though, those chips would be worth a fraction of their current value.

Huang has proven himself a tech CEO for the ages, and an AI visionary. He’s also been exceptionally good at turning that into money. But banking is a different business. And as Aschenbrenner just demonstrated, seeing around corners doesn’t always guarantee success.

Nvidia’s frenetic deal-making is just one of the ways in which the AI buildout has upended the traditional start-up financing playbook. As we noted last week, corporate venture investing has exploded in recent years, and accounted for almost 90% of all VC dollars that have gone into AI firms this year. That’s compared with less than 50% of VC dollars a decade ago.

New types of financial players are also making this cycle different.

From Wall Street to Jane Street

While hedge funds such as Coatue and mutual fund companies including Fidelity started getting involved in late-stage venture deals more than a decade ago, Situational Awareness was part of a fresh wave of investment firms encroaching on traditional VC territory.

The secretive Wall Street firm Jane Street, which traditionally made its money from proprietary trading algorithms, is on the cap tables of Anthropic, chipmakers Etched and MatX, and AI cloud infrastructure heavyweight Fluidstack. (It also had money with Aschenbrenner.)

Jane Street is interested in AI tech not just as investment, but for internal use: it invested $1 billion in CoreWeave and signed a $6 billion commitment to use its cloud platform for all of its machine learning needs. It’s also in the process of financing its own AI data center.

Big money managers and private credit firms including Goldman Sachs Asset Management, Blackstone, and Blue Owl Capital have also emerged as major funders of the AI buildout, extending loan packages in the tens of billions for data centers.

Part of this is simply a function of the enormous amounts of capital needed for AI data centers and chips. But for strategic investors, there are many other reasons to play the venture game.

Plumping up potential customers is one obvious benefit. An investment could also yield access to premier AI tools ahead of the competition, and help keep a funder abreast of the latest developments. There’s also the chance of a windfall: Google recorded an incredible $99 billion gain on its equity investments in Q2, mainly from gains on Anthropic and SpaceX.

“Enterprise software companies in particular often feel real urgency to invest in and sometimes acquire AI startups to stay competitive and defend their market share,” PitchBook senior research analyst Kaidi Gao told us.

Fear and greed continue to draw new corporate players into AI startup investing. Schneider Electric, for example, a 190-year old energy and industrials firm, is channeling capital from its €1 billion venture fund, SE Ventures, almost entirely into AI startups.

The $700 million Series A for Figure founder Brett Adcock’s new AI neolab Hark shows the new state of play: investors include Nvidia, AMD Ventures, ARK Invest, Brookfield, Intel Capital, Qualcomm Ventures, and Salesforce Ventures.


Newcomer Podcast

John Coogan on Selling TBPN to OpenAI, Sam Altman & the Future of Tech Media


One Big Chart

SPVs Outpace Direct Sales for Secondary Share Sales So Far this Year

Investors are increasingly turning to Special Purpose Vehicles, or SPVs, to purchase shares of Silicon Valley’s most sought-after private startups, rather than buying shares directly from employees or other investors.

A new PitchBook report showed that single-layer SPVs have surpassed direct-to-cap-table trades in secondary deal volume on the trading platform Caplight so far this year.

Read more

8

Stateless MCP has recaptured my interest (and inspired mcp-explorer and datasette-mcp)

Simon Willison · original → · 7/10 · Work/tech: Model Context Protocol 2.0 and MCP tools
Stateless MCP has recaptured my interest (and inspired mcp-explorer and datasette-mcp) 31st July 2026 Tuesday was Stateless MCP day—the rollout of MCP 2.0, or the 2026-07-28 Model Context Protocol…

Stateless MCP has recaptured my interest (and inspired mcp-explorer and datasette-mcp) 31st July 2026 Tuesday was Stateless MCP day—the rollout of MCP 2.0, or the 2026-07-28 Model Context Protocol specification to use the more formal but less memorable name. This is the most significant change to the MCP spec since it first launched, and has also served to reignite my personal interest in the protocol. For background: MCP is the Model Context Protocol, which describes a standard way to expose new tools to LLM-powered agent frameworks. It was introduced by Anthropic back in November 2024, had a huge spike of interest through much of 2025, and then became somewhat eclipsed by Skills (another Anthropic invention) when it became apparent that an agent harness with access to a terminal and curl could do most of what MCP did in a more flexible way. I wrote about that in my review of 2025. I’m coming back around to MCP now. Giving an agent a shell environment with the ability to access the internet is fraught with risk, and requires a strong model that is capable of effectively driving such an environment. MCP tools are easier to audit and control, and simple enough that smaller models that run on a laptop can still drive them reasonably well. The new stateless MCP specification also greatly decreases the complexity of implementing both clients and servers for the protocol. I built three of those this week! What’s easier with stateless MCP The best demonstration of the difference between stateful and stateless MCP is in this May 21st blog post that introduced the RC for the new specification. It included a clear before-and-after example. The older stateful MCP (I’m going to call it “legacy MCP”) required two HTTP requests—the first to initialize a session and obtain a Mcp-Session-Id , and the second to actually call the tool: POST /mcp HTTP/1.1 Content-Type: application/json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": { }, "clientInfo": { "name": "my-app", "version": "1.0" } } } POST /mcp HTTP/1.1 Mcp-Session-Id: 1868a90c-3a3f-4f5b Content-Type: application/json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "search", "arguments": { "q": "otters" } } } The new stateless way uses a single HTTP request which looks like this: POST /mcp HTTP/1.1 MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: search Content-Type: application/json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "search", "arguments": { "q": "otters" }, "_meta": { "io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0" } } } } This is so much cleaner from both a client- and server-side implementation perspective. It’s also a better fit for building scalable web applications, since now you don’t need to maintain server-side state to keep track of those session IDs, or worry about routing the same session to the same backend machine. mcp-explorer I couldn’t find a great CLI tool for interactively probing an MCP server, so I had Codex help build my own. mcp-explorer is the result. It’s a stateless Python CLI tool, so you don’t even need to install it to try it out—it works with uvx like this: uvx mcp-explorer list https://agentic-mermaid.dev/mcp This queries Ade Oshineye’s agentic-mermaid.dev demo MCP. The above command returns the following list of tools: execute(code: string, timeoutMs?: integer) - Execute Mermaid SDK code Run JavaScript in an isolated sandbox; return a value. describe_sdk(family: string, detail?: string) - Describe Mermaid SDK operations Return version-matched mutation operations for one diagram family. render_svg(source: string, options?: object) - Render Mermaid as SVG Render a Mermaid source string to themeable SVG. Returns { ok, svg }. render_ascii(source: string, useAscii?: boolean, targetWidth?: integer, options?: object) - Render Mermaid as text Render a Mermaid source string to text. Returns { ok, text }. render_png(source: string, scale?: number, background?: string, fitTo?: object, options?: object) - Render Mermaid as PNG Rasterize a Mermaid source string to PNG. Returns { ok, png_base64 }. ... Then to inspect a tool: uvx mcp-explorer inspect render_svg This outputs a whole bunch of information, including the JSON schema of the inputs and outputs. To call that tool and pass arguments to it: uvx mcp-explorer call \ https://agentic-mermaid.dev/mcp \ render_svg \ -a source 'graph TD; A-->B' \ -a options '{"padding":24}' Which returns: {"ok":true,"svg":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=... To get just the raw SVG try adding | jq .svg -r to that command. I got back this image: There are a few more commands in the README, but you get the general idea. I find building CLI tools like this to be a really productive way to get familiar with a specification, even if an agent writes most of the actual code. datasette-mcp The second project is datasette-mcp, a Datasette plugin which adds a /-/mcp endpoint to any Datasette instance. This is probably the fourth time I’ve tried building this plugin, but thanks to the new stateless MCP specification I finally have a version that feels good to release. It provides just three tools: list_databases() , get_database_schema(database_name) , and execute_sql(database_name, sql) . They do exactly what you would expect them to do—though execute_sql() is read-only for the moment. Wire these into an agent, or a chat tool like ChatGPT or Claude, and they’ll gain the ability to run SQL queries against your hosted Datasette instance. So far I’m running it on the Datasette mirror of my blog, at datasette.simonwillison.net/-/mcp. It took a bit of fiddling to figure out how to attach that to ChatGPT and Claude, but I got there in the end. Here’s a new TIL showing exactly how to do that. Here’s a shared Claude session where I asked it: list tables in simonwillison.net And then: what has Simon said recently about MCP? It ran 7 separate SQL queries to figure out the answer. llm-mcp-client My LLM tool is long overdue for an official MCP integration. The new alpha llm-mcp-client plugin is my attempt at exactly that: llm install llm-mcp-client llm -T 'MCP("https://datasette.simonwillison.net/-/mcp")' 'count the notes' Here’s the output (including reasoning trace, I’m using LLM 0.32rc2): Considering note count I see the question “count the notes” is probably asking me to tally up blog notes. It could also mean published notes or drafts, so there’s some ambiguity there. I’ll need to figure out the total number of notes, likely by querying the count for both published notes and drafts to get a clear answer. Let’s execute that count! There are 151 notes. And the output of llm logs for that prompt. Once this is fully baked, I’m considering bringing it directly into LLM core. I’m excited to experiment with MCP in Datasette Agent and llm-coding-agent as well. MCP is a safer way to build with agents A few months after MCP was first released, I wrote Model Context Protocol has prompt injection security problems, where I noted that the pattern of having end users mix and match tools pushed responsibility for avoiding data exfiltration attacks out to the users themselves. I hadn’t coined the Lethal Trifecta yet, but that was absolutely what I had in mind. Then general agents with arbitrary shell and curl access came along, and that’s so much harder to keep secure! Something I’ve come to appreciate about MCP is that it’s much easier to reason about agent capabilities and what might go wrong than with arbitrary command execution in an open network environment—the default for most of today’s general and coding agent tools. I plan to lean into MCP a whole lot more when I’m building sensitive applications on top of LLMs.

Items scoring 7/10 or above from 11 sources, scored by claude-haiku-4-5-20251001 on relevance to my interests. At most 3 per source.

Scoring categories & sources
  1. Local Wexford or South East Ireland news
  2. Irish or EU-wide affairs affecting citizens broadly: elections, new laws or policy being debated, cost of living, education — especially impacts on mid-life adults or teenagers. Never courts/crime stories.
  3. Irish news on a topic relevant to my interests
  4. Work and tech topics: networking, AI, Kubernetes, platforms, SaaS
  5. AI news including critical or anti-AI perspectives
  6. Gaming: PC gaming, indie gaming, retro gaming
  7. General interests: gardening, woodwork, cycling, fitness, travel
  8. Comics

Sources: Breaking News Ireland, Wexford Local, Hacker News, r/gaming, r/pcgaming, r/antiAI, r/indiegaming, Lenny's Newsletter, One Useful Thing, Newcomer, Simon Willison

Comics

Main Span

XKCD · view →
Wind stress? Don

Wind stress? Don