Kernel Portability — the snvme module across Linux versions and GPU vendors
How the
snvmekernel module stays buildable across kernel lineages and GPU vendors, and the userspace/kernel contract that keeps the pair in lockstep.
1. Two maintained kernel trees
tutti/device_manager/nvme/kernel_modules/
├── snvme-5.4.241-1-tlinux4-0017/ # tlinux4 production lineage
└── snvme-5.15.0-public/ # upstream-ish public baseline
Both trees are built from this repository; third_pkgs/ holds the
read-only upstream mirror for reference/diffing. Shared logic lives in
identical files across the two trees (map.c, peer_memory/,
compat.c); version/vendor differences are isolated into exactly two
units per tree:
Unit |
Sole owner of |
Isolates |
|---|---|---|
|
|
kernel API drift (e.g. |
|
vendor P2P headers ( |
GPU-vendor P2P mapping — one |
Rule of thumb: map.c and everything above never #if on kernel
version and never name a GPU vendor symbol directly — they call
compat_* / peer_memory_ops.*.
2. The peer_memory backend layer (GPU P2P)
GPU page pinning and DMA mapping go through an ops table:
Opaque types —
peer_page_table,peer_dma_mappinghide the vendor’s page-table representation frommap.c; accessors replace direct->entries/->dma_addresseswalks.One backend per vendor —
peer_memory/nvidia.cwrapsnvidia_p2p_get_pages/nvidia_p2p_dma_map_pages;peer_memory/metax.cis the symmetric Metax backend. Exactly one backend is selected at compile time throughTUTTI_P2P_BACKEND; that backend resolves its vendor symbols dynamically (__symbol_get) during module initialization.Build isolation — only the backend
.cincludes vendor headers; CMake discovers the selected backend’s header directory and passes it to Kbuild. CUDA defaults tonvidia; MUSA/MACA default tometax.
3. Userspace ABI handshake
The ioctl UAPI is versioned (tutti/include/uapi/tutti_snvme.h):
TUTTI_SNVME_ABI_VERSIONis negotiated at attach; a module older than the library’s minimum is rejected fail-closed (ENODEV) — never a silent fallback to mismatched layouts.Consequence: swapping kernel modules requires rebuilding userspace (
libnvm,tutti_daemon) against the matching headers. The root production build (cmake --build --preset cuda-module --target libnvm tutti_daemon modules) produces the matched set together.UAPI structs are plain-C layout-stable;
libnvmcompiles its device headers under both nvcc and plain C via layout-identical fallbacks for the GPU-side atomic fields.
4. Queue geometry — controller maximum
Queue depth always takes the controller maximum:
q_depth = NVMe CAP.MQES + 1(no module parameter). Userspace rings always follow the controller-reported depth — there is no userspace override (a smaller userspace ring would desynchronize SQ wrap-around and CQ phase tracking against the controller’s deeper rings).Queue count: up to 32 queue pairs per queue group (
NVM_MAX_QUEUES_PER_GROUP); userspace requests beyond that are clamped. Both CPU-side (daemon) and GPU-side queues are allocated dynamically after mount via batched ioctl.The kernel allocates queue objects with the full installed depth regardless of what any single client asks for.
5. Interrupt semantics under GPU-polled completions
User CQs are polled by GPU threads (phase-bit), so the kernel’s interrupt
handler routinely finds a CQ already consumed by the GPU. The handler
reports IRQ_HANDLED for that case — returning IRQ_NONE would let the
kernel’s spurious-interrupt detector storm-disable the IRQ line. This is
a deliberate semantic: GPU-consumed ≠ spurious.
6. Build & test entry points
Production matched set:
cmake --build --preset cuda-module --target libnvm tutti_daemon modulesproducesbuild/cuda-module/module/snvme{,-core}.koplus the CUDA userspace targets.Baseline matrix: each tree compiles against its own headers; cross-compiling a tree against a different lineage’s headers is a known-incompatible configuration (NVMe core API drift), not a compat defect.
Runtime order is strict: insmod → daemon → mount — the
/dev/snvme*block devices exist only after daemon bring-up.