Skip to content

feat(sglang): enable HiCache L3 file backend - #4213

Merged
Tanguille merged 18 commits into
mainfrom
feat/sglang-hicache-l3
Jul 27, 2026
Merged

feat(sglang): enable HiCache L3 file backend#4213
Tanguille merged 18 commits into
mainfrom
feat/sglang-hicache-l3

Conversation

@Tanguille

@Tanguille Tanguille commented Jul 27, 2026

Copy link
Copy Markdown
Owner

L3 was recorded as failing restart recovery on 07-15. That was the wrong conclusion: two defects sat upstream of the measurement, neither a property of L3.

  1. The hash extension JIT-compiles hash_binding.cpp on the first prefill and could not find <openssl/sha.h> in the runtime stage. Lazy, so it passed startup and health checks then killed the scheduler mid-request.
  2. write_through_selective gates promotion on hit_count >= 2, so first-pass content never reached L2 and L3 had nothing to back up.

Retested 2026-07-27 on sha256:72d934d3 under plain write_through.

stage evidence
backend initialises synthetic prefill served; 22 min under live traffic, 0 restarts
L3 writes 268,176 files / 14 GB, backuped_tokens_total 267,978
L2 saturates hicache_host_used_tokens 267,978 of 274,861
survives restart cold start then prefetched_tokens_total{storage_backend="file"} 63,205

L2 accounting closes exactly: 73,120 newly backed + 63,205 prefetched = 136,325 hicache_host_used_tokens, so every L2 token is either a fresh write or a read-back from disk.

Populate (58,071 tok) PP 196.9 tok/s. Extension probe (58,209 tok) PP 252.9 tok/s, TG 0.56 tok/s. Both ran with production traffic on the GPU, so both are floors. Reuse is validated with an extension probe, not a resend, because DeltaNet hybrids report #cached-token: 0 on identical resends by design.

Sizing

The evictor defaults to unbounded and openebs-hostpath enforces no quota, so without a cap L3 grows until control-1's 500G root fills. MAX_SIZE=64Gi bounds the cache; MIN_FREE_SPACE=100Gi is the independent guard, since a cache under its own cap can still push the node past kubelet's ~50G nodefs threshold when other tenants grow.

L3 gets its own PVC instead of sharing the 10Gi triton cache, so the declared size matches what it holds. The class forbids expansion, so a nominal request could not be corrected later.

Notes

Summary by CodeRabbit

  • New Features

    • Added persistent file-backed caching for the Qwen 36B inference service.
    • Improved cache recovery across restarts, including verified prefetched content.
  • Bug Fixes

    • Corrected cache revalidation guidance to prevent corrupted files from being treated as valid.
    • Improved artifact download handling to avoid truncation and unnecessary full re-downloads.
  • Documentation

    • Clarified cache testing results, operational storage considerations, and known limitations.
    • Documented fixes for misleading restart-recovery and cache-integrity measurements.

… headers

The ai namespace had accumulated config that restates what the API server or
the chart already supplies, plus comments that outlived the thing they
described.

Removed only fields proven inert:

- VirtualMCPServer serviceType/db and HTTPRoute parentRefs group/kind +
  PathPrefix matches are CRD/Gateway-API defaults. Confirmed with
  `kubectl apply --dry-run=server`: the API server refills every one, so the
  stored objects are unchanged. `config.aggregation` was deliberately NOT
  removed - it is not defaulted when the parent object is absent, and it
  controls tool-name prefixing.
- llmkube-models `timeout: 60m` never applied: the cluster-apps patch
  strategic-merges `timeout: 5m` onto every child Kustomization. `flate diff`
  shows no rendered change from deleting it.
- llmkube `prometheusRule.enabled: false` and litellm's three secretKeyRef
  blocks restate a chart default and an envFrom that the CRD supports.
- memini dependsOn llmkube-models was already implied through litellm.

Comment fixes: the model-cache comment named the two iGPU nodes backwards
(qwen3-embedding is on control-2, not control-3), a memory note cited a
revision sha that appears nowhere in the repo, and the /dev/dri
supplementalGroups block was explained verbatim in three files.

Repo-wide consistency: 115 missing `yaml-language-server` schema headers added
across 71 files (core/v1 kinds skipped - that host serves no schema for them),
and the nine raw-githubusercontent kustomization schema URLs normalised to
json.schemastore.org.

Docs: deleted the completed sglang-oci-cutover plan per its own exit criterion,
repointed three links at a directory that no longer exists, and corrected the
sglang pin from v0.5.14 to the v0.5.15 actually running.
Five MCPToolConfigs carried `toolsFilter: []`, which the CRD documents as
"If empty, all tools are exposed" - they filtered nothing, and the ten
toolConfigRef fields pointing at them were dead weight.

Verified on the live cluster rather than from the doc text, because the two
paths could have diverged in the operator. Removed the toolConfigRef from
MCPServer kubesearch with `kubectl patch` and re-ran an MCP tools/list against
the vmcp-resources gateway: 23 tools before, 23 after, names byte-identical,
including all ten kubesearch_* entries. Live state has been restored.

That test also retired the reason this was previously skipped. The concern was
that restarting the MCP servers would park the gateways for several minutes;
the observed recovery was 34 seconds, and the aggregating gateway pods never
restarted at all.

NOT removed, because these three do real work despite looking similar:
- grafana: a genuine 33-tool allow list.
- karakeep and searxng: no toolsFilter at all - they use toolsOverride to
  rename tools (search-bookmarks -> search_bookmarks). Dropping those refs
  would have renamed eleven live tools.

The orphaned `github` MCPToolConfig is left alone here; that file is rewritten
on feat/github-mcp-http.
…n experiments

Renamed sglang-benchmarks.md to engine-benchmarks-gfx1201.md: the file was never
SGLang-specific - it is a vLLM / SGLang / llama.cpp comparison for Qwen3.6-27B on
RDNA4, and its 2026-06-21 round actually concluded in favour of vLLM.

Restored the "Open / next experiments" list, which the previous commit wrongly
deleted as stale scaffolding. Five of those are outstanding work, not history.
Added the SGLang revalidation item: when upstream rebases the RDNA4 patch series
past our FORK_REF pin, re-run the bench/ concurrency sweep and verbatim test and
compare against the recorded v0.5.15 numbers before moving the production pin.

Reframed the header from "SUPERSEDED / historical" to a dated measurement series
with a live experiment queue, and dropped only the generic Process Instructions
boilerplate. Updated the one inbound link in sglang-blockers.md.
Flux's CEL status evaluator returns InProgressStatus when no expression
matches (fluxcd/pkg runtime/cel/status_evaluator.go, final return), so an
inProgress expression only earns its place if its truth set overlaps failed
or current.

Here it cannot. The InferenceService phase enum is Pending, Creating,
Progressing, Ready, WaitingForGPU, Stopped, Suspended, Failed. inProgress
matched Pending/Creating/Progressing/WaitingForGPU, failed matches Failed,
current matches Stopped or Ready+Available - three disjoint sets over one
scalar field, so every phase lands on the same verdict with the expression
removed. Suspended already fell through to InProgress and still does.

Also brings this in line with the only two other healthCheckExprs in the repo
(cert-manager, rook-ceph), which both use failed + current alone.
…espace

# Conflicts:
#	kubernetes/apps/ai/llmkube/models/vmcp-embedding.yaml
Review caught that sglang-blockers.md and docker/sglang-rdna4/README.md
disagreed about the retired PVC-rebuild script. The README also told readers to
keep the Dockerfile in sync with a file that no longer exists, and linked the
cutover doc this branch removes.

Both now name the exact recovery command instead of gesturing at git history:
`git show b8f12ae^:kubernetes/apps/ai/sglang/app/scripts/sglang-env-rebuild.sh`,
which was checked and returns all 151 lines. The "keep them in sync" instruction
is gone, since the Dockerfile is now the only build path.
L3 was recorded as failing restart recovery on 07-15. Two defects sat upstream
of that measurement: the hash extension could not JIT-compile without OpenSSL
headers in the runtime stage, and write_through_selective gated promotion on
hit_count >= 2 so first-pass content never reached L2 for L3 to back up.

Retested 07-27 on sha256:72d934d3 under plain write_through. After a cold
restart, prefetched_tokens_total{storage_backend="file"} read 63,205, and L2
accounting closes exactly: 73,120 newly backed + 63,205 prefetched = 136,325
hicache_host_used_tokens.

L3 gets its own PVC rather than sharing the triton cache, and the evictor is
bounded: it defaults to unbounded, and openebs-hostpath enforces no quota.
MIN_FREE_SPACE guards the node's shared root independently of cache size.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Tanguille, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b7577801-4723-4767-9a9f-d24191d179be

📥 Commits

Reviewing files that changed from the base of the PR and between 35b3a2f and adf3ffa.

📒 Files selected for processing (3)
  • docs/llm-hosting/sglang-blockers.md
  • docs/llm-hosting/vllm-vs-sglang-2026-07.md
  • kubernetes/apps/ai/llmkube/models/qwen36-27b-sglang.yaml
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: enabling the SGLang HiCache L3 file backend.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sglang-hicache-l3

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

✅ Created PR with unit tests: #4214

…espace

# Conflicts:
#	kubernetes/apps/ai/toolhive/config/github.yaml
…espace

# Conflicts:
#	kubernetes/apps/observability/siren/app/helmrelease.yaml
…se one

opencode renders from app-template, and the repo pairs that chart with the
bjw-s schema in 52 of 53 cases.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/llm-hosting/vllm-vs-sglang-2026-07.md`:
- Around line 187-189: Update the downloader guidance in the referenced passage
to state that comparing HEAD Content-Length only detects truncation and is not
cache validation. Explicitly retain conditional GET plus digest or format
validation, and require the “revalidated” status to be logged only when the
server returns an actual 304 response.

In `@kubernetes/apps/ai/llmkube/models/qwen36-27b-sglang.yaml`:
- Around line 134-149: Update the HiCache configuration around the `hicache`
volume and `/hicache` mount so each model/cache-format revision uses an isolated
revisioned subdirectory, or otherwise ensure the `qwen36-27b-hicache` PVC is
rotated whenever incompatible model, tokenizer, or cache-format changes roll
out. Preserve the existing cache volume wiring while preventing startup scans
from reusing stale pages across revisions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b0c1ddb7-e47e-4eaf-bacc-cae2256d3398

📥 Commits

Reviewing files that changed from the base of the PR and between cab9b5c and 35b3a2f.

📒 Files selected for processing (3)
  • docs/llm-hosting/sglang-blockers.md
  • docs/llm-hosting/vllm-vs-sglang-2026-07.md
  • kubernetes/apps/ai/llmkube/models/qwen36-27b-sglang.yaml
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Tanguille/LLMKube (auto-detected)

Comment thread docs/llm-hosting/vllm-vs-sglang-2026-07.md Outdated
Comment thread kubernetes/apps/ai/llmkube/models/qwen36-27b-sglang.yaml
The page filename is sha256(token_ids, prior_hash, page_size) plus
_{model_name}_{tp_rank}_{tp_size}, so it encodes neither the weights revision nor
the KV layout. Renovate bumps both the AWQ revision and the sglang image, and
either would have served KV pages computed by the previous one.

Also drop the claim that a Content-Length compare validates the download cache.
@Tanguille

Copy link
Copy Markdown
Owner Author

Both remarks were valid. Resolved in 70b5fae4f.

HiCache stale-page reuse: confirmed and fixed. Traced the key derivation in v0.5.15 rather than assuming. get_hash_str is sha256(token_ids, prior_hash, page_size) (mem_cache/utils.py:106), and the on-disk name appends config_suffix = f"_{model_name}" plus TP/PP/CP ranks (hicache_storage.py:336-346). Nothing encodes the weights revision, the KV dtype or the KV layout.

That is exploitable here specifically because served-model-name is stable while Renovate bumps both mutable inputs underneath it: the AWQ revision (hf://mattbucci/Qwen3.6-27B-AWQ@f541031d) and the sglang image. Either bump would have served KV pages computed by the previous one, so this is a wrong-output risk rather than a cache-miss. STORAGE_DIR is now /hicache/sglang-v0.5.15_awq-f541031d.

Caveat worth stating plainly: that suffix is bumped by hand, so it can rot. It sits directly above the pinned image and revision it tracks to keep the two visible together, but the durable fix is for the backend to fold a model/engine fingerprint into config_suffix upstream.

Content-Length: reworded. The point stands, with one correction to the framing. The primary fix is $dest.tmp + mv, since the actual bug is that curl truncates the valid cached copy before it knows whether it needs the body. A HEAD size compare is a cheap truncation preflight, not cache validation, and it supplements the conditional GET rather than replacing it. Reworded to say exactly that.

Base automatically changed from chore/simplify-ai-namespace to main July 27, 2026 19:44
# Conflicts:
#	kubernetes/apps/ai/llmkube/models/qwen36-27b-sglang.yaml
# Conflicts:
#	kubernetes/apps/ai/llmkube/models/qwen36-27b-sglang.yaml
@tanguille-cluster

Copy link
Copy Markdown
@@ spec.env @@
# inference.llmkube.dev/v1alpha1/InferenceService/ai/qwen36-27b
! + three list entries added:
+ - name: SGLANG_HICACHE_FILE_BACKEND_STORAGE_DIR
+   value: /hicache/sglang-v0.5.15_awq-f541031d
+ - name: SGLANG_HICACHE_FILE_BACKEND_MAX_SIZE
+   value: 64Gi
+ - name: SGLANG_HICACHE_FILE_BACKEND_MIN_FREE_SPACE
+   value: 100Gi

@@ spec.extraArgs @@
# inference.llmkube.dev/v1alpha1/InferenceService/ai/qwen36-27b
! - one list entry removed:
- - write_through_selective
! + three list entries added:
+ - write_through
+ - "--hicache-storage-backend"
+ - file

@@ spec.extraVolumeMounts @@
# inference.llmkube.dev/v1alpha1/InferenceService/ai/qwen36-27b
! + one list entry added:
+ - name: hicache
+   mountPath: /hicache

@@ spec.extraVolumes @@
# inference.llmkube.dev/v1alpha1/InferenceService/ai/qwen36-27b
! + one list entry added:
+ - name: hicache
+   persistentVolumeClaim:
+     claimName: qwen36-27b-hicache

@@ (root level) @@
# v1/PersistentVolumeClaim/ai/qwen36-27b-hicache
! + one document added:
+ apiVersion: v1
+ kind: PersistentVolumeClaim
+ metadata:
+   name: qwen36-27b-hicache
+   namespace: ai
+   labels:
+     kustomize.toolkit.fluxcd.io/name: llmkube-models
+     kustomize.toolkit.fluxcd.io/namespace: ai
+ spec:
+   resources:
+     requests:
+       storage: 64Gi
+   storageClassName: openebs-hostpath
+   accessModes:
+   - ReadWriteOnce

@Tanguille
Tanguille merged commit 4546719 into main Jul 27, 2026
14 checks passed
@Tanguille
Tanguille deleted the feat/sglang-hicache-l3 branch July 27, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant