CODEBASE ORIENTATION

Orientation: antirez/ds4

A 15-minute working mental model of a 259,000-line C inference engine

PURPOSE

antirez/ds4 is a local inference engine for DeepSeek 4 Flash and Pro — a C program that runs a very large mixture-of-experts model on one machine, with hand-written kernels for Metal, CUDA and ROCm and experts streamed from disk when the model does not fit in memory. MIT, ~19k stars, first published 2026-05-06.

THE COMPREHENSION PROBLEM

81 files, 1,864 symbols, and no package manifest to orient you. Roughly 259,000 lines of C, Objective-C, CUDA, HIP and Metal. Four files are over 10,000 lines; ds4.c alone is 64,541. There is no src/, no module directory, no layer diagram. Reading in file order tells you nothing.

What follows is the 15-minute version: the seams that matter, then two exercises. Answers are hidden until you click — write yours down first, being wrong before you read the answer is the part that makes it stick.

HOW THIS WAS MADE

Four commands did the structural work. The only bytes of ds4.c I looked at were the 13 lines a symbol query pulled out of it. The orienting-codebases skill runs the same structural pipeline as an agent-facing codebase review — tree-sitter symbol scan, feature gather, then targeted source extraction by symbol name — and spends the output on exercises for a human instead of an analysis dump for the model.

treesit.py /tmp/ds4 --stats                      # 81 files, 1,864 symbols, 1.6s
gather.py  /tmp/ds4 --skip tests --source-budget 8000
treesit.py /tmp/ds4 --no-tree 'source:ds4_compute_layer_placement'
treesit.py /tmp/ds4 --no-tree 'symbols:ds4.h'

Every line of code below was extracted from the tarball by symbol name and escaped mechanically — none of it was retyped from memory, and the file:line references are the tool's, not mine. Two facts in my first draft were guesses: the line counts of two files, and how the ROCm build relates to the CUDA path. Both were replaced with what wc and grep actually returned. Everything in the answer keys is checkable against the repo at the cited lines.

Pedagogy adapted from DrCatHicks/learning-opportunities: hidden answers, generation before feedback, fading scaffolds. Prompted by Cat Hicks on codebase comprehension.

Key files

ds4.h 477 lines

The entire public contract. Opaque ds4_engine / ds4_session handles, an options struct, tokenizer and chat-template calls. Read this first — it is under 0.2% of the source in the repo and defines everything the five binaries are allowed to touch.

ds4.c 64,541 lines

The engine. Model load, tokenizer, sampling, KV cache, session sync, expert routing, memory estimation. One translation unit. Do not read it top-down; enter it from a symbol you already care about.

ds4_gpu.h 2,603 lines

The backend interface — every GPU operation the engine can ask for, as plain C declarations. The seam that keeps ds4.c free of Metal, CUDA and HIP types.

ds4_metal.m + metal/*.metal 39,615 + 21,671 lines

Apple backend: Objective-C host code plus hand-written Metal shaders.

ds4_cuda.cu 27,666 lines

NVIDIA backend. Same header, entirely different implementation.

ds4_rocm.cu + rocm/*.cuh 216 + 26,139 lines

AMD backend. The .cu file is a 216-line preamble; the kernels are in the .cuh headers it includes.

ds4_cli.c / ds4_server.c / ds4_agent.c / ds4_bench.c / ds4_eval.c 2,205 / 17,539 / 11,185 / 822 / 4,312 lines

Five main()s, five binaries, one engine. The Makefile links each against the same CORE_OBJS.

ds4_ssd.c + ds4_streaming_hotlist.inc 210 + 13,334 lines

MoE experts streamed from disk. The .inc is a generated table of hot (layer, expert) pairs, sorted by profiled hit rate.

ds4_layer_pack.c 149 lines

Multi-GPU placement. The whole layer-to-device decision is 32 lines (Exercise 1).

linenoise.c, rax.c 2,690 + 2,747 lines

antirez's own line editor and radix tree, vendored. The radix tree backs the server's prompt-prefix KV store.

Core concepts

Engine vs session

ds4_engine owns the model weights and the device; ds4_session owns one conversation's KV cache and logits. The header comment says the split exists so HTTP and CLI code never depends on tensor internals — callers hand over a full token prefix and ds4_session_sync() decides whether to reuse, extend, or rebuild graph state.

One narrow header, three enormous backends

ds4_gpu.h declares the operations; ds4_metal.m, ds4_cuda.cu and ds4_rocm.cu each implement all of them. The backend is chosen at link time by swapping one object file in CORE_OBJS, not at runtime by a vtable.

Experts stream from SSD

The model does not fit in RAM, so routed experts are read from the model file on demand and a subset is cached. ds4_ssd_auto_cache_plan() converts a byte budget into an expert count; ds4_streaming_hotlist.inc supplies which experts to preload, generated from profiling runs rather than reasoned about at runtime.

Placement decisions are small and readable

The parts that look like they need the most cleverness — which layers go on which GPU, how many experts to cache — are short arithmetic functions in tiny files. The bulk of the line count is kernels and protocol handling, not policy.

Vendored, not depended on

linenoise.c, rax.c, and the GGUF tooling are in-tree. The Makefile has no package manager step: make on a Mac produces five binaries against system frameworks.

Orientation exercises

EXERCISE 1 OF 2

Where do the layers go?

CONTEXT

ds4 can split a model across several GPUs and spill the remainder to CPU. All of that policy lives in one function in a 149-line file. entry_bytes[] holds the size of each layer in model order; device_for_entry[] is the output.

CODE
/* ds4_layer_pack.c:14-45 */
int ds4_compute_layer_placement(const size_t *entry_bytes,
                                int n_entries,
                                const ds4_layer_pack_config *cfg,
                                int *device_for_entry) {
    if (!entry_bytes || !cfg || !device_for_entry) return 1;
    if (n_entries < 0) return 2;
    if (cfg->n_gpus < 0 || cfg->n_gpus > DS4_LAYER_PACK_MAX_GPUS) return 3;

    /* Work over a local copy of budgets so the caller's struct is untouched. */
    size_t budget[DS4_LAYER_PACK_MAX_GPUS];
    for (int d = 0; d < cfg->n_gpus; d++) {
        budget[d] = cfg->gpu_budget_bytes[d];
    }

    int d = 0;
    for (int e = 0; e < n_entries; e++) {
        /* Advance until the current device can hold this entry, or we run
         * out of devices. Strict greater-than: exact fits stay on d. */
        while (d < cfg->n_gpus && entry_bytes[e] > budget[d]) {
            d++;
        }
        if (d < cfg->n_gpus) {
            device_for_entry[e] = d;
            budget[d] -= entry_bytes[e];
        } else {
            device_for_entry[e] = DS4_LAYER_PACK_CPU;
            /* d stays at cfg->n_gpus so every subsequent entry also lands
             * on the CPU tier. */
        }
    }
    return 0;
}
Your turn Device 0 fills up on entry 12. Entry 13 is small enough to fit in what is left on device 0 — where does it go, and why did antirez write it that way?
KEY POINTS

Entry 13 goes to device 1, not device 0. d only ever advances; the while loop never rewinds it, so once the walker leaves a device it never comes back — even when a later, smaller layer would fit in the gap.

Each device therefore gets a contiguous run of layers. Inference walks layers in order, so contiguous placement means the activation crosses a device boundary once per device instead of ping-ponging. A tighter bin-packing would waste less VRAM and cost far more PCIe traffic.

The same one-way walk produces the CPU behaviour the comment calls out: once an entry falls through to DS4_LAYER_PACK_CPU, d is parked at n_gpus and every later entry lands on CPU too. The tail of the model runs on CPU, not a random scatter of leftovers.

EXERCISE 2 OF 2

Find the missing backend

CONTEXT

The repo advertises three GPU backends: Metal, CUDA, ROCm. Here is the complete backend enum from the public header, the two opaque handles beneath it, and the header's own statement of intent.

CODE
/* ds4.h:11-23 */
/* Public engine boundary.
 *
 * The CLI and server should treat ds4_engine as the loaded model and
 * ds4_session as one mutable inference timeline.  A session owns the live KV
 * cache and logits; callers provide full token prefixes and let
 * ds4_session_sync() reuse, extend, or rebuild the graph state.  Keep this
 * header narrow so HTTP/CLI code does not depend on tensor internals. */

typedef enum {
    DS4_BACKEND_METAL,
    DS4_BACKEND_CUDA,
    DS4_BACKEND_CPU,
} ds4_backend;

/* ds4.h:59-60 */
typedef struct ds4_engine ds4_engine;
typedef struct ds4_session ds4_session;
Your turn There is no DS4_BACKEND_ROCM. If you had to find how an AMD build works, where would you look — and what would you expect to find?
KEY POINTS

ROCm is not a runtime backend at all. It is a build-time substitution, and the trail runs through the Makefile, not the header.

# Makefile — strix-halo target
strix-halo:
	$(MAKE) -B ds4 ds4-server ds4-bench ds4-eval ds4-agent \
		CORE_OBJS="ds4.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_rocm.o ds4_rocm_compat.o ds4_rocm_unavailable.o ds4_layer_pack.o" \
		CFLAGS="$(CFLAGS) -DDS4_ROCM_BUILD" \
		DS4_LINK="$(HIPCC) $(ROCM_CFLAGS)" \
		DS4_LINK_LIBS="$(ROCM_LDLIBS)"

rocm: strix-halo

make strix-halo swaps ds4_cuda.o out of CORE_OBJS for ds4_rocm.o, links with hipcc, and defines DS4_ROCM_BUILD. ds4_rocm.h then rewrites the CUDA API onto HIP with a wall of macros:

/* ds4_rocm.h */
#include <hip/hip_runtime.h>
#include <hipblas/hipblas.h>
#include <hip/hip_fp16.h>
#include <hipcub/hipcub.hpp>
#include <rocwmma/rocwmma-version.hpp>
#include <rocwmma/rocwmma.hpp>
...
#define cudaError_t hipError_t
#define cudaStream_t hipStream_t
#define cudaEvent_t hipEvent_t
#define cudaDeviceProp hipDeviceProp_t
#define cudaMemLocation hipMemLocation
#define cudaMalloc hipMalloc
#define cudaMallocHost hipHostMalloc
#define cudaMallocManaged hipMallocManaged
#define cudaFree hipFree
#define cudaFreeHost hipFreeHost
#define cudaMemset hipMemset

So an AMD binary still selects DS4_BACKEND_CUDA at runtime. The only place the difference surfaces to a user is here:

/* ds4.c:47258 */
const char *ds4_backend_name(ds4_backend backend) {
    switch (backend) {
    case DS4_BACKEND_METAL: return "metal";
    case DS4_BACKEND_CUDA:
#ifdef DS4_ROCM_BUILD
        return "rocm";
#else
        return "cuda";
#endif
    case DS4_BACKEND_CPU:   return "cpu";
    }
    return "unknown";
}

Three consequences worth carrying: the enum is a link-time choice, not a dispatch table, so one binary cannot switch backends; #ifdef DS4_ROCM_BUILD is the string to grep when AMD behaviour diverges (it appears in the CLI, the server and ds4_gpu.h); and the CPU build is a fourth path with its own object set, which is why make cpu compiles every .c a second time into *_cpu.o.

In C, the configuration another language would put in a plugin registry usually lives in the build system. When a header does not contain the concept you were promised, look there next.

Where to go next

THREE THREADS WORTH PULLING
  • ds4_ssd_auto_cache_plan() in ds4_ssd.c:108 — 28 lines that turn a RAM budget into a number of cached experts. Then ask where ds4_streaming_hotlist.inc comes from.
  • ds4_session_sync() in ds4.c — the prefix-reuse decision that makes a chat server fast. Start from the header comment, not the definition.
  • ds4_kvstore.c plus rax.c — how the server finds the longest cached prefix of an incoming prompt. A radix tree doing exactly what radix trees are for.
Composed with composing-html