--- name: pcie-link-capability-probe description: "Measure a GPU-to-host PCIe link the way a weight-streaming engine experiences it, and convert the result into a tokens/s ceiling (streamed expert weights) versus the compute-in-RAM alternative — before recommending or rejecting an offload design." --- # PCIe link capability probe Use when someone asks "is the link fast enough to run model X with engine Y (on one GPU)?" and Y keeps some weights in host RAM — MoE expert offload with an LRU GPU cache, layer paging, weight streaming. The answer is arithmetic once the link is measured, and the *shape* of the engine decides whether the link matters at all: | engine shape | what crosses PCIe per token | link matters? | |---|---|---| | streams weights (expert LRU, paging) | the missed weights (~GB/token) | **yes — decisive** | | computes offloaded weights in RAM (llama.cpp `-ot exps=CPU`) | activations only (~KB) | no; host RAM bandwidth decides | | everything resident on the GPU | nothing | no | ## 1. Establish the hardware ceiling (never trust the card alone) ```bash lspci -tv | grep -iA2 vga # which bridge the GPU hangs off d=$(lspci -D | grep -i nvidia | awk '{print $1}' | head -1) sudo lspci -vv -s "$d" | grep -E 'LnkCap:|LnkSta:' # the CARD's capability + trained state sudo lspci -vv -s 00:03.1 | grep -E 'LnkCap:|LnkSta:' # the PLATFORM PORT (this is the real limit) ``` - `LnkCap` of the **upstream port** = the electrical ceiling. A card with `Width x16` under an `x8` port cannot exceed x8; an x4 riser/cable caps both. - `LnkSta` at idle usually shows a downgraded speed (PCIe ASPM downshift, e.g. `2.5GT/s (downgraded)`) — that is NOT the loaded rate. Trust the measurement in step 2. - Gen4 usable payload: x4 **7.877 GB/s**, x8 15.75, x16 31.5 (128b/130b encoding). ## 2. Measure it (the program that did the job) Stop **all** production services on the host first (fleet policy), then compile and run: ```cuda // pcie-link-test.cu : nvcc -O2 -o /tmp/pcie-link-test /tmp/pcie-link-test.cu #include #include #include #include #define CK(x) do{ cudaError_t e=(x); if(e!=cudaSuccess){fprintf(stderr,"CUDA %s (%s:%d)\n",cudaGetErrorString(e),__FILE__,__LINE__); return 1;} }while(0) static double ms_between(cudaEvent_t a, cudaEvent_t b){float ms=0; cudaEventElapsedTime(&ms,a,b); return (double)ms;} static int bench_size(size_t bytes){ const int iters = bytes>=(1UL<<30)?10:(bytes>=(256UL<<20)?20:100); void *dev=NULL,*host=NULL,*pageable=NULL; cudaStream_t s=NULL; CK(cudaMalloc(&dev,bytes)); CK(cudaHostAlloc(&host,bytes,cudaHostAllocDefault)); memset(host,0x5a,bytes); CK(cudaStreamCreate(&s)); cudaEvent_t a,b; CK(cudaEventCreate(&a)); CK(cudaEventCreate(&b)); for(int i=0;i<3;i++) CK(cudaMemcpy(dev,host,bytes,cudaMemcpyHostToDevice)); CK(cudaDeviceSynchronize()); CK(cudaEventRecord(a)); for(int i=0;i10× slower than the memory behind it (x4 is 31× here), streaming loses by construction and no tuning rescues it; doubling the link (x4 → x8) only doubles a losing number. ## Worked example (Dankclaw 2026-09-16) Gen4 x4, measured H2D 6.72 / D2H 6.60 GB/s (85 % of spec), 3 MiB slab = 474 µs. A3B MoE, Q4_K_M (75 GiB experts, ~1.70 GiB active/token): 272 ms/token ⇒ **3.7 tok/s** (4.6 with 20 % resident). Same host, same model, measured: **20.46 tok/s** with the experts computed on the CPU, 25 (halogen), 26.5 (mainline on the iGPU). Verdict: the streaming design loses 3-5×, and the engine in question could not load the GGUF anyway. ## Pitfalls - Stop production services before running this (fleet policy) — and remember the 3090's VRAM may be almost fully occupied, so a 2 GiB test buffer needs the service stopped. - Don't quote a derived line from inside the program without checking it: a printf format/argument mismatch in this very probe printed a nonsense ceiling. Recompute by hand. - GiB vs GB: convert before dividing (1.70 GiB = 1.825 GB). - The idle `LnkSta` (2.5 GT/s, downgraded) is not the loaded link — quoting it makes a working x4 look like a broken x1.