All notes

First Experiments With Serverless GPUs and the 128k Context I Was Advertising Was Really 16k Each

June 18, 2026 ·5 min read

My first time serving a model on a rented per-second GPU instead of my own card. vLLM wouldn't load the GGUF file, the CUDA build had to link against stub libraries because image builds get no GPU attached, and the context flag I thought I understood was dividing my window across concurrent request slots.

llm inferencellama.cppgpuserverlessdeployment

I run a 27B Qwen3.6 community finetune locally on a 32 GB card, which forces a quantisation and context-length compromise I don’t love. So I tried the obvious upgrade: rent a 96 GB GPU by the second, serve the higher-precision Q8_0 file from it, and keep the 128k context window the model supports.

This was my first real go at serverless GPU hosting — Modal specifically, though most of what bit me is not Modal-specific. The pitch is that you write a Python file describing a container and a GPU, deploy it, and get an HTTP endpoint that scales to zero when nobody is using it. That part was genuinely as easy as advertised. The things that cost me the day were all downstream of one structural difference from running on my own machine: the environment that builds your image is not the environment that runs it. Locally those are the same computer, and every assumption I didn’t know I was making came from that.

It works, and the finished config does 42.67 tokens/sec of generation. Three things surprised me on the way there, and only one of them was about the GPU.

vLLM couldn’t load the file, for a reason that isn’t about vLLM

First attempt was vLLM, because it’s the default answer for serving a model to more than one caller at a time. It has an experimental GGUF loader. It refused the file:

ValueError: GGUF model with architecture qwen35 is not supported yet.

The instinct is to go read vLLM’s supported-models list, find Qwen3, and be confused — because Qwen3 is supported. But that error isn’t coming from vLLM’s model registry. vLLM’s GGUF path delegates checkpoint parsing to Hugging Face transformers, so the question isn’t “does my server support this architecture,” it’s “does the library my server hands the file to support this architecture.” Two different lists, and the second one is invisible from the documentation of the first.

The generalisable version: when a loader is described as experimental, find out what it delegates to before you debug it as if it were native. Architecture support for a format and architecture support for a model family are separate things, and a community finetune of a recent model tends to sit exactly in the gap. I stopped fighting it and moved to llama-server from llama.cpp, which reads GGUF as its native format rather than as an import path.

The image builder links CUDA it can’t see

llama-server with CUDA support means building it, and building it into the container image so it isn’t recompiled on every cold start. The GPU is attached when the container runs, not when the image is built — that’s how every serverless GPU platform works, and it’s correct, since you shouldn’t be billed for an H100 to run cmake. But it means the build step has no /dev/nvidia*, no driver, and therefore no real libcuda.so.1 to link against:

/usr/bin/ld: warning: libcuda.so.1, needed by libggml-cuda.so, not found

This one has a clean answer that I always forget exists. CUDA ships stub libraries in /usr/local/cuda/lib64/stubs — symbol-complete, functionally empty. They exist so you can link at build time on a machine that will never run the binary. Point the linker at them and the build completes:

LIBRARY_PATH=/usr/local/cuda/lib64/stubs
-DCMAKE_SHARED_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda"
-DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda"

At runtime the real driver library is present and takes precedence, so the stub is a build-time fiction only. Do not copy the stub directory into the runtime library path “to be safe” — a binary that resolves libcuda to the stub will start cleanly and then fail in ways that look like hardware problems.

Two other build flags mattered more than I expected:

  • -DGGML_CUDA_FA_ALL_QUANTS=OFF — the default compiles a FlashAttention kernel for every combination of KV cache quantisation types. Turning it off took the CUDA build from roughly five minutes to about twenty seconds. I use one KV type; I was paying for dozens.
  • -DCMAKE_CUDA_ARCHITECTURES="89;90" — naming the two architectures I actually target instead of letting CMake fan out across every generation it knows.

Both are the same lesson, which is that an image build you run once still costs you every time you change the line above it. A four-and-a-half-minute compile is a four-and-a-half minute wait on every iteration of the surrounding config, and that’s where the whole day goes.

The flag I had wrong

Here is the part worth the post.

I had the server started with a 128k context window and eight parallel request slots, and I wrote that up as “128k context, 8 concurrent users.” Both flags were doing what the documentation says. What the documentation says is not what I had assumed:

-c 131072      # total KV cache budget
-np 8          # split across this many slots

-c is not per-request context. It’s the size of the whole KV cache, and -np divides it. 131072 / 8 = 16,384 tokens per user. I had built a 128k-context server and then partitioned it into eight sessions with a 16k window each — smaller than the 32 GB workstation config I was trying to improve on. Nothing errored. Each slot just silently has one-eighth of the number in the flag I was quoting.

I dropped to -np 4, which gives four concurrent sessions of 32k each, and that’s the real trade: concurrency and context length are the same budget, and you can’t quote both numbers at full value in the same sentence. If you serve long-context requests, you serve few of them at once. Pick which one the workload actually needs before you size the GPU, because the GPU is the thing you’re paying for and it can’t tell you which way you got this wrong.

The VRAM split shows where the money goes. Of about 87 GB used on a 96 GB card:

Component Approx VRAM
Q8_0 weights, 27B params 28.5 GB
KV cache, 131,072 tokens at f16 56.0 GB
CUDA context and overhead 2.5 GB

The cache is twice the model. That ratio is why the context/concurrency question is the sizing decision and the weights are almost a footnote — and it’s an argument for testing quantised KV before buying a bigger card, which I haven’t done yet.

What I left on the table

The honest number: 42.67 tokens/sec generation, 145.35 tokens/sec prompt evaluation, from the server’s own telemetry.

My 32 GB workstation does 136 tokens/sec on the same model family. The rented 96 GB card is three times slower, and I know why — the multi-token prediction head I benchmarked last week is present in this GGUF file and I never passed the flag that uses it. I picked the MTP build of the model and then ran it as an ordinary one.

So treat 42.67 as a floor rather than a result. It’s a correct config, not a tuned one, and the difference between those two is a flag I already knew about.