-
Notifications
You must be signed in to change notification settings - Fork 5
RAG Workload
This page documents the synthetic RAG workload, used for KV-cache tiering experiments. For the workload that replays real captured agent traces, see OTel Trace Replay.
Ships with ready-to-run configs in configs/, in three KVC-tier variants:
| Config suffix | kvc_tiers |
Purpose |
|---|---|---|
-HBM |
(none — APC only) | GPU APC only, no tiered KV-cache storage. Baseline. |
-HBM-DRAM |
["CPUMemory"] |
Two-tier: GPU APC + CPU DRAM. |
-HBM-DRAM-DFS |
["CPUMemory", "DistributedFS"] |
Three-tier: GPU APC + CPU DRAM + DistributedFS. |
Run any of them with:
python -m opal.main -c configs/RAG_KVCtiers-HBM-DRAM-DFS.jsonType string: "RAGWorkload" (class RAGWorkload, opal/workloads/rag_workload.py)
Simulates a synthetic retrieval-augmented-generation workload designed to exercise prefix-cache reuse across a fixed document corpus.
- At init, a shared system prompt (
system_prompt_sizetokens) is hashed once and reused as the prefix of every request. - A corpus (pool) of
num_documentsdocuments (eachdocument_sizetokens) is generated. - Each request selects
docs_per_requestdistinct documents at random, sorts them and concatenates them:system_prompt + doc_1 + doc_2 + .... - Requests are submitted at
request_rate(requests/sec), optionally withjitterfor inter-arrival variance, up tomax_concurrent_requestsin flight at once.
Because documents are reused across requests but combined in different random subsets, the workload produces partial prefix matches (system prompt always hits; documents hit only when re-selected) — useful for testing tiered KV-cache lookup that has to find non-contiguous chunks across tiers.
| Parameter | Default | Description |
|---|---|---|
num_documents |
10 |
Size of the document corpus to sample from. |
document_size |
16384 |
Tokens per document. |
system_prompt_size |
1024 |
Tokens in the shared system prompt (prepended to every request). |
docs_per_request |
4 |
Number of distinct documents concatenated per request. Must be ≤ num_documents. |
output_tokens |
128 |
Fixed decode/output length for every request. |
request_rate |
1.0 |
Requests per second (mean, before jitter). |
jitter |
0.0 |
Inter-arrival variance; 0 = fixed interval, up to 1.0 = high variance. |
total_requests |
-1 |
Stop after this many requests (-1 = unbounded, run until simulation_time/time_duration_sec). |
time_duration_sec |
-1 |
Stage-local timeout in virtual seconds. |
max_concurrent_requests |
16 |
Caps in-flight requests; generation blocks until one completes. |
"workload": {
"stages": [
{
"type": "RAGWorkload",
"workload_params": {
"num_documents": 10,
"document_size": 16384,
"system_prompt_size": 1024,
"docs_per_request": 4,
"output_tokens": 128,
"request_rate": 0.25,
"jitter": 0.0,
"total_requests": 1000,
"time_duration_sec": -1,
"max_concurrent_requests": 1
}
}
]
}With these settings, total context per request = 1024 + 4*16384 = 66,560 tokens, with the 1024-token system prompt and any previously-selected documents eligible for cache reuse.
Since each request sorts its selected documents before concatenating them, document order doesn't matter — the number of distinct document-subset prefixes the workload can produce is the binomial coefficient C(num_documents, docs_per_request). This is the size of the pool of unique non-system-prompt prefixes that total_requests draws from (with repetition): the smaller it is relative to total_requests, the more often a given combination — and hence its KV-cache entries — gets reused.
Pool size (num_documents) |
docs_per_request=3 |
docs_per_request=4 |
docs_per_request=5 |
|---|---|---|---|
| 10 | 120 | 210 | 252 |
| 20 | 1,140 | 4,845 | 15,504 |
| 30 | 4,060 | 27,405 | 142,506 |
| 40 | 9,880 | 91,390 | 658,008 |
| 50 | 19,600 | 230,300 | 2,118,760 |
| 60 | 34,220 | 487,635 | 5,461,512 |
| 70 | 54,740 | 916,895 | 12,103,014 |
| 80 | 82,160 | 1,581,580 | 24,040,016 |
| 90 | 117,480 | 2,555,190 | 43,949,268 |
| 100 | 161,700 | 3,921,225 | 75,287,520 |
Each request draws one of the N = C(num_documents, docs_per_request) unique prefixes from the table above, uniformly at random with replacement. This is the classic coupon collector's problem: assuming an infinite KV-cache pool (so nothing is ever evicted), the question is how many requests, on average, it takes to have seen a given fraction p of the N distinct prefixes at least once.
Going from having seen j distinct prefixes to j+1 takes, in expectation, N / (N - j) requests (the probability the next draw is new is (N-j)/N). Summing that from j=0 to m-1 (where m = p*N) gives the expected number of requests to reach fraction p:
E[requests to reach p] = N * (H(N) - H(N-m)) where H(n) = n-th harmonic number, m = p*N
≈ N * ln(1 / (1-p)) for large N
The approximation only depends on p, not on N — it just scales linearly with the pool size:
| Fraction of prefixes seen | Multiplier (× N) |
|---|---|
| 50% |
ln(2) ≈ 0.69 |
| 75% |
ln(4) ≈ 1.39 |
| 95% |
ln(20) ≈ 3.00 |
In other words, you need roughly 0.7×N requests to see half of all unique prefixes, 1.4×N to see three-quarters, and 3×N to see 95% — the long tail of rarely-drawn combinations dominates. Concretely, for docs_per_request=3,4,5:
docs_per_request=3
| Pool size | N |
Requests for 50% | Requests for 75% | Requests for 95% |
|---|---|---|---|---|
| 10 | 120 | 83 | 165 | 350 |
| 20 | 1,140 | 790 | 1,579 | 3,406 |
| 30 | 4,060 | 2,814 | 5,627 | 12,153 |
| 40 | 9,880 | 6,848 | 13,695 | 29,588 |
| 50 | 19,600 | 13,585 | 27,170 | 58,707 |
| 60 | 34,220 | 23,719 | 47,437 | 102,504 |
| 70 | 54,740 | 37,942 | 75,884 | 163,977 |
| 80 | 82,160 | 56,948 | 113,896 | 246,120 |
| 90 | 117,480 | 81,430 | 162,860 | 351,929 |
| 100 | 161,700 | 112,081 | 224,162 | 484,400 |
docs_per_request=4
| Pool size | N |
Requests for 50% | Requests for 75% | Requests for 95% |
|---|---|---|---|---|
| 10 | 210 | 145 | 292 | 630 |
| 20 | 4,845 | 3,359 | 6,716 | 14,510 |
| 30 | 27,405 | 18,996 | 37,991 | 82,094 |
| 40 | 91,390 | 63,346 | 126,694 | 273,780 |
| 50 | 230,300 | 159,631 | 319,262 | 689,908 |
| 60 | 487,635 | 338,003 | 676,007 | 1,460,829 |
| 70 | 916,895 | 635,544 | 1,271,088 | 2,746,777 |
| 80 | 1,581,580 | 1,096,267 | 2,192,534 | 4,737,981 |
| 90 | 2,555,190 | 1,771,122 | 3,542,246 | 7,654,666 |
| 100 | 3,921,225 | 2,717,987 | 5,435,972 | 11,746,936 |
docs_per_request=5
| Pool size | N |
Requests for 50% | Requests for 75% | Requests for 95% |
|---|---|---|---|---|
| 10 | 252 | 174 | 348 | 757 |
| 20 | 15,504 | 10,746 | 21,492 | 46,440 |
| 30 | 142,506 | 98,777 | 197,556 | 426,906 |
| 40 | 658,008 | 456,096 | 912,191 | 1,971,214 |
| 50 | 2,118,760 | 1,468,612 | 2,937,224 | 6,347,228 |
| 60 | 5,461,512 | 3,785,631 | 7,571,262 | 16,361,230 |
| 70 | 12,103,014 | 8,389,170 | 16,778,341 | 36,257,394 |
| 80 | 24,040,016 | 16,663,269 | 33,326,537 | 72,017,458 |
| 90 | 43,949,268 | 30,463,311 | 60,926,621 | 131,660,239 |
| 100 | 75,287,520 | 52,185,332 | 104,370,663 | 225,541,244 |
Note this is an upper bound on real warm-up time in practice: a finite/evicting cache (the -HBM-DRAM / -HBM-DRAM-DFS configs) reaches a different steady state — recently-evicted prefixes can be re-requested as cache misses even after their first hit, so tiered runs don't strictly converge to "95% of prefixes seen" in the same sense. The numbers above describe the warm-up of the unbounded case only, useful as a lower bound on total_requests needed before steady-state hit-rate statistics become meaningful.