, ,

Token Economics and Latency Tuning for Self Hosted Granite (Red Hat Gen AI Series, Part 24)

What one answer from a self hosted Granite model actually costs, and the three vLLM flags that decide it. A latency aware guide to throughput, TTFT and cost per token on the Red Hat AI Inference Server.

Red Hat Gen AI Series · Part 24 of 30
Key takeaways: A self hosted GPU costs the same whether it runs at 5 percent or 95 percent load, so cost per token is set almost entirely by how full you keep the card, not by the model. On one H100 serving Granite 8B FP8, moving average utilisation from 30 to 60 percent roughly halves the cost per million output tokens. Throughput and latency pull in opposite directions: raising max-num-seqs lifts tokens per second but pushes time to first token past a target fast, so the cheapest config that still meets your latency budget sits at a knee, not at maximum batch. Tune three flags, gpu-memory-utilization, max-num-seqs and max-num-batched-tokens, measure against a latency target, and stop there.
Who this is for: An architect who owns the GPU bill and a latency target for the self hosted assistant, running Granite on the Red Hat AI Inference Server from Part 20 behind llm-d from Part 23. Assumes you can read the vLLM startup banner from Part 21 and quantised to FP8 in Part 22. Terms on first use: throughput is output tokens per second across all users; TTFT is time to first token; TPOT is time per output token during decode; utilisation here means the fraction of wall clock time the GPU spends generating tokens; max-num-seqs is the cap on concurrent sequences in a batch; max-num-batched-tokens is the cap on tokens processed per scheduler step.

Finance asked me a fair question about the assistant: what does one answer cost? My honest reply was that it depends almost entirely on how busy the GPU is at the moment the answer is generated. A card sitting at 5 percent load still draws its full hourly rate, so an idle accelerator is the most expensive way to serve a token. Everything in this part follows from that one fact.

Cost of a GPU you already paid for

Last part moved the assistant onto llm-d so shared prefixes stopped paying for prefill twice. This part turns the throughput and latency numbers that change produced into a cost per answer, and tunes the handful of flags that decide how few GPUs the deployment needs.

Start with the number that dominates everything else: the hourly cost of the accelerator. An owned H100, amortised over three years with power and hosting, lands near 1.80 dollars an hour. Rent the same card on demand in a cloud and you pay closer to 3.00 dollars an hour. That rate is fixed. It does not fall when traffic is light. So the only lever that moves cost per token is how many tokens the card generates in each of those hours, and that is a function of two things you control, peak throughput and average utilisation.

Peak throughput is what the model can produce when fully loaded. Utilisation is what fraction of the day it actually runs that hot. Most internal assistants sit near 20 to 30 percent because traffic follows office hours, and that idle time is pure waste billed at the full rate. This is the same argument the AI Engineering Series makes about cost control and model routing, only here the meter is a card you own rather than a per token API bill, and it never pauses.

Three numbers that set serving cost

Red Hat documents four metrics for judging an inference deployment, and three of them decide the money. Output token throughput, in tokens per second across all requests, sets how quickly the card empties its work queue. Time to first token measures the wait a user feels before anything appears, and it is dominated by the prefill pass that builds the KV cache from the prompt. Time per output token is the steady decode speed once generation starts. Throughput is the cost axis; TTFT and TPOT are the experience axis. You cannot read one from the other, which is why a single tokens per second figure quoted with no latency is close to meaningless.

To ground these, here is a real serving benchmark from the Red Hat AI Inference Server documentation running a small FP8 model on one card: about 21.7 requests per second, 8,775 output tokens per second, mean TTFT 193 milliseconds and mean TPOT 9 milliseconds. Those are the shape of the numbers. An 8B model like our Granite serves fewer tokens per second and a higher TTFT than that 1B example, which is exactly what the next section measures.

Measuring where the assistant stands

Serve the quantised Granite from Part 22 with an explicit set of tuning flags, so nothing is left to a default you did not choose. Read the token and the pull secret from the environment, never the command file.

# Tested against Red Hat AI Inference Server 3.2 (vLLM 0.10.x upstream),
# Granite 3.3 8B Instruct quantised to FP8 in Part 22, one NVIDIA H100 80GB,
# CUDA 12. HF_TOKEN comes from the environment, not the manifest.
podman run --rm -it --device nvidia.com/gpu=all --shm-size=8GB -p 8000:8000 
  --env "HF_TOKEN=${HF_TOKEN}" 
  -v ./rhaiis-cache:/opt/app-root/src/.cache 
  --security-opt=label=disable 
  registry.redhat.io/rhaiis/vllm-cuda-rhel9:3.2.0 
  --model RedHatAI/granite-3.3-8b-instruct-FP8 
  --gpu-memory-utilization 0.90 
  --max-num-seqs 64 
  --max-num-batched-tokens 2048 
  --max-model-len 8192

Watch the startup banner, because it prints the two numbers that decide how much concurrency the card can hold. GPU KV cache size is how many tokens of cache fit after weights are loaded, and maximum concurrency is how many full length requests that supports at once.

INFO ... Using Flash Attention backend.
INFO ... GPU KV cache size: 244,912 tokens
INFO ... Maximum concurrency for 8,192 tokens per request: 29.90x
INFO ... Starting vLLM API server on http://0.0.0.0:8000

Now drive load at the server with the vLLM benchmark script and read the result. Running 200 prompts capped at 64 concurrent requests, each 1,024 tokens in and 512 out, gives the picture at our chosen batch ceiling.

$ python vllm/benchmarks/benchmark_serving.py --backend vllm 
    --model RedHatAI/granite-3.3-8b-instruct-FP8 
    --dataset-name random --random-input 1024 --random-output 512 
    --num-prompts 200 --max-concurrency 64 --port 8000

============ Serving Benchmark Result ============
Successful requests:                     200
Benchmark duration (s):                  34.71
Output token throughput (tok/s):         2902.44
Total Token throughput (tok/s):          8802.13
Request throughput (req/s):              5.76
---------------Time to First Token----------------
Median TTFT (ms):                        243.10
P99 TTFT (ms):                           471.88
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms):                          16.83
==================================================

Read the P99 TTFT of 472 milliseconds carefully. If your target is a first token inside 300 milliseconds, this config already fails it at the tail even though median looks fine. That gap between median and P99 is where most latency complaints actually live, and it widens as batch size grows.

Throughput and latency pull against each other

Drop the concurrency cap to 32 and rerun, and the trade becomes visible in two lines. Throughput falls but the tail latency comes back under target.

$ python vllm/benchmarks/benchmark_serving.py ... --max-concurrency 32
Output token throughput (tok/s):         2118.90
Median TTFT (ms):                        188.20
P99 TTFT (ms):                           278.65
Mean TPOT (ms):                          13.40

So 32 concurrent meets a 300 millisecond tail at 2,119 tokens per second, while 64 concurrent breaks it for only 37 percent more throughput. Push concurrency higher and throughput barely climbs while TTFT runs away, because the card saturates and new requests queue behind the ones already decoding. Plotting the whole sweep shows a clear knee, and the cheapest safe config sits at it, not past it.

Throughput versus P99 latency as batch size growsGranite 8B FP8 on one H100, max-num-seqs from 8 to 256, figures illustrative01100 ms2200 ms016003200 tok/s300 ms TTFT targetseqs 8seqs 32seqs 64seqs 128seqs 256output throughput (tok/s)
Past a knee near 32 to 64 concurrent sequences, extra batch buys almost no throughput while P99 time to first token climbs through the target. The cheapest safe config is the last point still under the red line.
War story: I pushed gpu-memory-utilization to 0.97 to fit more KV cache and cut the assistant from three GPUs to two. It held through a load test at 32 concurrent, so I shipped it. Two days later a burst of about 60 requests, one of them a 12k token document paste, tipped a card into CUDA out of memory mid decode and the on call got paged at 9 on a Saturday. My 3 percent headroom was not enough for a long prompt whose activation peak lands on top of a nearly full cache. I dropped back to 0.90, added chunked prefill, and kept the two GPUs. One Saturday and one page to learn that the last few percent of GPU memory is not free space, it is a crumple zone.

Tuning knobs that move the numbers

Four flags do almost all of the work. Raising gpu-memory-utilization gives vLLM more room for KV cache, which raises the concurrency the card can hold, until a long prompt runs it out of memory. Raising max-num-seqs lifts throughput and pushes TTFT up. Max-num-batched-tokens governs how many tokens a scheduler step processes, so higher values favour prefill and first token latency while lower values favour smoother inter token latency, and it must stay at or above max-num-seqs. Max-model-len sets the context ceiling, and larger values reserve more cache per sequence, which quietly cuts how many sequences fit. Keep this reference next to the serve command.

FlagRaise it toLower it toGotcha
gpu-memory-utilizationfit more KV cache, hold more concurrencyleave OOM headroom for long promptsabove about 0.92 a long prompt can OOM mid decode
max-num-seqspush output throughputprotect P99 TTFThigh values blow the tail latency target
max-num-batched-tokensimprove prefill and first token latencysmooth inter token latencymust stay at or above max-num-seqs
max-model-lensupport longer contextfree cache for more concurrencylarge values cut maximum concurrency sharply

Here is the failure that lands most often when you get greedy with the first flag. Setting gpu-memory-utilization to 0.97 alongside a 16k context left no room for a real long prompt.

$ podman run ... --gpu-memory-utilization 0.97 --max-model-len 16384 ...
ERROR ... EngineCore encountered a fatal error.
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 1.94 GiB.
GPU 0 has a total capacity of 79.15 GiB of which 1.07 GiB is free.

# Cause: at 0.97 the KV cache plus a 12k token prompt activation peak left
# no headroom. Fix: give the card room back, and cap context if you can.
$ podman run ... --gpu-memory-utilization 0.90 --max-model-len 8192 ...

Chunked prefill, on by default in the vLLM V1 engine you read about in Part 21, softens this by slicing large prefills into pieces and prioritising decode, which steadies inter token latency under load. It is one of the few settings where the default is also the right answer for most traffic.

Where the cost model misleads you

Vendor tuning guides and the vLLM docs both push you toward maximum throughput: raise max-num-seqs, raise max-num-batched-tokens, fill the card. That advice is wrong the moment a latency target binds. Maximum throughput and minimum cost per token are not the same point once you refuse to ship a first token slower than, say, 300 milliseconds. Past the knee in the chart above you buy a few percent more tokens per second at the price of a doubled tail latency, which either violates the target or forces you to shed load, and shed load is just idle capacity wearing a different hat. The cheapest config is the highest throughput that still clears your latency target, and nothing beyond it.

Translate throughput into money and the utilisation point becomes concrete. At roughly 3,000 output tokens per second peak for Granite 8B FP8 on one H100, the cost per million output tokens depends far more on how full you keep the card than on which card it is. This table is the artifact I bring to the budget conversation.

Average utilisationOwned H100 at 1.80/hrCloud H100 at 3.00/hr
15 percent (nights and weekends idle)1.11 per million tokens1.85 per million tokens
30 percent (typical office hours)0.56 per million tokens0.93 per million tokens
60 percent (batched and time shifted)0.28 per million tokens0.46 per million tokens
90 percent (steady queued load)0.19 per million tokens0.31 per million tokens

Figures illustrative, at about 3,000 output tokens per second peak on one H100. Cost per million output tokens equals the hourly rate divided by tokens generated per hour, then divided by utilisation.

Two readings jump out. Moving from 30 to 60 percent utilisation halves the unit cost, which is a bigger win than most model or hardware swaps deliver, and it costs nothing but scheduling. And a self hosted card only beats a hosted API on price once utilisation is genuinely high; at 15 percent you are paying more per token than many managed endpoints charge, with none of their elasticity. The decision to self host should rest on data residency and control, the reason this assistant cannot call a hosted API at all, not on a cost claim that only holds at utilisation you have not yet reached. The GenAI cost breakdown frames the same trade from the buyer side, and the Data Science Series works the raw hardware maths in GPU cost, scale and sizing.

flowchart LR
  G[GPU hourly cost] --> C[Cost per million tokens]
  T[Peak tokens per second] --> C
  U[Average utilisation] --> C
  C --> V[Falls fast as utilisation rises]
Unit cost decomposes into three inputs. Only utilisation is a scheduling choice, and it is the one with the most leverage.

Cutting the assistant GPU bill

For this assistant I would fix the latency target first, then find the highest max-num-seqs that clears it, which on our traffic was 32, not the 64 I started with. I would hold gpu-memory-utilization at 0.90 and resist the urge to chase the last few percent, because a Saturday page taught me what that space is for. And I would attack utilisation, not throughput, by routing overnight batch jobs and document reindexing onto the same card so it stops idling at the office hours rate. Higher utilisation is the cheapest performance win on the table, and it needs no new hardware.

Do this on Monday: Run the vLLM benchmark twice, at max-concurrency 32 and 64, and write down the P99 TTFT at each. Pick the higher one that still clears your latency target and set max-num-seqs there. Then pull one week of traffic and compute your real average utilisation; if it is under 40 percent, move a batch workload onto the same GPU before you even think about buying another. Verdict: tune to the latency knee and raise utilisation. Avoid the maximum throughput config the docs steer you toward when a latency target binds, and avoid gpu-memory-utilization above 0.92 on any endpoint that accepts long prompts.

Next part builds the benchmark harness properly, turning these one off measurements into a repeatable load test you can run before every model or config change, so a silent regression never reaches production unmeasured.

Red Hat Gen AI Series · Part 24 of 30
« Previous: Part 23  |  Guide  |  Next: Part 25 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading