Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 56 additions & 12 deletions api/v1alpha1/cachebackend_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
CacheBackendTypeExternal CacheBackendType = "External"
)

// +kubebuilder:validation:Enum=Redis;LMCacheServer;Mooncake
// +kubebuilder:validation:Enum=Redis;LMCacheServer;Mooncake;NFS

// CacheBackendRemoteStorageProvider identifies the technology used for the
// optional shared/remote cache tier.
Expand All @@ -40,6 +40,7 @@ const (
CacheBackendRemoteStorageProviderRedis CacheBackendRemoteStorageProvider = "Redis"
CacheBackendRemoteStorageProviderLMCacheServer CacheBackendRemoteStorageProvider = "LMCacheServer"
CacheBackendRemoteStorageProviderMooncake CacheBackendRemoteStorageProvider = "Mooncake"
CacheBackendRemoteStorageProviderNFS CacheBackendRemoteStorageProvider = "NFS"
)

// +kubebuilder:validation:Enum=Managed;External
Expand Down Expand Up @@ -133,6 +134,18 @@ const (
SGLangHiCacheMemoryPageHead SGLangHiCacheMemoryLayout = "page_head"
)

// +kubebuilder:validation:Enum=best_effort;wait_complete;timeout

// SGLangHiCacheStoragePrefetchPolicy controls when a storage-tier prefetch
// stops before request execution continues.
type SGLangHiCacheStoragePrefetchPolicy string

const (
SGLangHiCacheStoragePrefetchBestEffort SGLangHiCacheStoragePrefetchPolicy = "best_effort"
SGLangHiCacheStoragePrefetchWaitComplete SGLangHiCacheStoragePrefetchPolicy = "wait_complete"
SGLangHiCacheStoragePrefetchTimeout SGLangHiCacheStoragePrefetchPolicy = "timeout"
)

// SGLangHiCacheSpec configures SGLang's native, engine-local host-memory cache.
// Exactly one of SizeGB and Ratio must be set. Optional tuning fields are
// passed to SGLang only when explicitly configured, so the engine version owns
Expand Down Expand Up @@ -162,6 +175,11 @@ type SGLangHiCacheSpec struct {
// MemoryLayout maps to --hicache-mem-layout.
// +optional
MemoryLayout SGLangHiCacheMemoryLayout `json:"memoryLayout,omitempty"`

// StoragePrefetchPolicy maps to --hicache-storage-prefetch-policy when
// remoteStorage.provider=NFS. It is rejected without that storage tier.
// +optional
StoragePrefetchPolicy SGLangHiCacheStoragePrefetchPolicy `json:"storagePrefetchPolicy,omitempty"`
}

// CacheBackendHostMemorySpec configures engine-side host memory. Capacity is
Expand Down Expand Up @@ -248,6 +266,24 @@ type MooncakeRemoteStorageSpec struct {
Resources *corev1.ResourceRequirements `json:"resources,omitempty"`
}

// NFSRemoteStorageSpec identifies an existing NFS export mounted into each
// selected engine Pod. NFS is externally owned; inference-cache creates no
// mount target, export, PV, PVC, or StorageClass.
type NFSRemoteStorageSpec struct {
// Server is the NFS mount-target hostname or IP address.
// +kubebuilder:validation:MinLength=1
Server string `json:"server"`

// Path is the absolute path exported by the NFS server.
// +kubebuilder:validation:MinLength=1
Path string `json:"path"`

// MountPath is the absolute path at which the export is mounted in the
// engine container.
// +kubebuilder:validation:MinLength=1
MountPath string `json:"mountPath"`
}

// CacheBackendRemoteStorageSpec configures the optional shared/remote tier.
// Omitting this object in the canonical API requests an engine-local,
// host-only hierarchy and never implicitly provisions infrastructure.
Expand All @@ -259,8 +295,9 @@ type CacheBackendRemoteStorageSpec struct {
// workload or connects to operator-managed infrastructure.
Ownership CacheBackendRemoteStorageOwnership `json:"ownership"`

// Endpoint is required for External ownership and rejected for Managed
// ownership, whose endpoint is controller-observed.
// Endpoint is required for network providers with External ownership and
// rejected for Managed ownership, whose endpoint is controller-observed.
// NFS is mounted from its typed server/path fields and has no endpoint.
// +optional
Endpoint string `json:"endpoint,omitempty"`

Expand All @@ -275,6 +312,11 @@ type CacheBackendRemoteStorageSpec struct {
// Mooncake contains Mooncake-owned configuration.
// +optional
Mooncake *MooncakeRemoteStorageSpec `json:"mooncake,omitempty"`

// NFS identifies an externally owned NFS export and its engine-container
// mount path. It is valid only with provider=NFS and ownership=External.
// +optional
NFS *NFSRemoteStorageSpec `json:"nfs,omitempty"`
}

// CacheBackendObservationSpec configures KV-event observation independently
Expand Down Expand Up @@ -658,21 +700,23 @@ type CacheBackendIntegrationSpec struct {
FirstEventTimeout *metav1.Duration `json:"firstEventTimeout,omitempty"`

// FailOpen controls whether the engine treats cache lookups as a soft
// dependency. When true (the default), an unreachable or degraded cache
// backend MUST fall back to local prefill and never fail a serving
// request — the cache is an optimization, not a serving dependency. When
// explicitly set to false the engine fails requests on cache
// unreachability ("fail-closed"); the cache becomes a serving
// dependency, which is loud and visible via a Warning Event on the
// owning CacheBackend.
// dependency. For integrations admitted with true (the default), an
// unreachable or degraded cache backend MUST fall back to local prefill
// and never fail a serving request — the cache is an optimization, not a
// serving dependency. When explicitly set to false the cache becomes a
// fail-closed serving dependency, which is loud and visible via a Warning
// Event on the owning CacheBackend.
//
// The flag is plumbed by the engine adapter as INFERENCECACHE_FAIL_OPEN
// (both shipping LMCache adapters — vLLM+LMCache and SGLang+LMCache —
// inject it). Per-request fail-open enforcement at the engine level is the
// engine/connector's responsibility; the cache plane surfaces the bit so
// the engine can honor it.
// SGLangHiCache accepts only the default true value and does not inject this
// env var because native HiCache exposes no equivalent fail-closed control.
// Host-only SGLangHiCache accepts the default true value and does not inject
// this env var because native HiCache exposes no equivalent control.
// NFS-backed SGLangHiCache instead requires an explicit false value: kubelet
// mounts the inline NFS volume before containers start, so export
// availability is inherently a serving dependency.
// +optional
// +kubebuilder:default=true
FailOpen *bool `json:"failOpen,omitempty"`
Expand Down
27 changes: 23 additions & 4 deletions api/v1alpha1/cachebackend_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,17 @@ func TestCacheBackendCRDSchemaFieldsAndEnums(t *testing.T) {
remoteStorageSchema := mustProperty(t, specSchema, "remoteStorage")
requireRequired(t, remoteStorageSchema, "provider")
requireRequired(t, remoteStorageSchema, "ownership")
requireEnum(t, mustProperty(t, remoteStorageSchema, "provider"), []string{"Redis", "LMCacheServer", "Mooncake"})
requireEnum(t, mustProperty(t, remoteStorageSchema, "provider"), []string{"Redis", "LMCacheServer", "Mooncake", "NFS"})
requireEnum(t, mustProperty(t, remoteStorageSchema, "ownership"), []string{"Managed", "External"})
for _, field := range []string{"endpoint", "redis", "lmCacheServer", "mooncake"} {
for _, field := range []string{"endpoint", "redis", "lmCacheServer", "mooncake", "nfs"} {
if !hasProperty(remoteStorageSchema, field) {
t.Fatalf("spec.remoteStorage.%s is missing from CRD schema", field)
}
}
nfsSchema := mustProperty(t, remoteStorageSchema, "nfs")
for _, field := range []string{"server", "path", "mountPath"} {
requireRequired(t, nfsSchema, field)
}
observationSchema := mustProperty(t, specSchema, "observation")
for _, field := range []string{"modelID", "firstEventTimeout"} {
if !hasProperty(observationSchema, field) {
Expand Down Expand Up @@ -141,6 +145,11 @@ func TestCacheBackendCRDSchemaFieldsAndEnums(t *testing.T) {
"page_first_kv_split",
"page_head",
})
requireEnum(t, mustProperty(t, hiCacheSchema, "storagePrefetchPolicy"), []string{
"best_effort",
"wait_complete",
"timeout",
})
firstEventTimeoutSchema := mustPath[map[string]any](t, integrationSchema, "properties", "firstEventTimeout")
if got, ok := firstEventTimeoutSchema["default"].(string); !ok || got != "5m" {
t.Fatalf("integration.firstEventTimeout default = %v, want \"5m\"", firstEventTimeoutSchema["default"])
Expand Down Expand Up @@ -266,6 +275,11 @@ func TestCacheBackendDeepCopyCopiesNestedFields(t *testing.T) {
Limits: corev1.ResourceList{corev1.ResourceMemory: providerMemory},
},
},
NFS: &NFSRemoteStorageSpec{
Server: "10.0.0.25",
Path: "/hicache",
MountPath: "/mnt/hicache",
},
},
Observation: &CacheBackendObservationSpec{
ModelID: "model-a",
Expand All @@ -286,8 +300,9 @@ func TestCacheBackendDeepCopyCopiesNestedFields(t *testing.T) {
MatchLabels: map[string]string{"inferencecache.io/cache-enabled": "true"},
},
HiCache: &SGLangHiCacheSpec{
SizeGB: &hiCacheSize,
WritePolicy: SGLangHiCacheWriteThrough,
SizeGB: &hiCacheSize,
WritePolicy: SGLangHiCacheWriteThrough,
StoragePrefetchPolicy: SGLangHiCacheStoragePrefetchWaitComplete,
},
BackendConfig: map[string]string{"evictionPolicy": "LRU"},
Template: &CacheBackendPodSpecOverride{
Expand Down Expand Up @@ -336,6 +351,7 @@ func TestCacheBackendDeepCopyCopiesNestedFields(t *testing.T) {
*backend.Spec.LMCache.WorkerPort = 6666
backend.Spec.RemoteStorage.LMCacheServer.Command[0] = "changed"
backend.Spec.RemoteStorage.LMCacheServer.Resources.Limits[corev1.ResourceMemory] = resource.MustParse("4Gi")
backend.Spec.RemoteStorage.NFS.Server = "changed"
backend.Spec.Observation.ModelID = "changed"
backend.Spec.Observation.FirstEventTimeout.Duration = time.Hour
backend.Spec.Integration.FirstEventTimeout.Duration = time.Hour
Expand Down Expand Up @@ -386,6 +402,9 @@ func TestCacheBackendDeepCopyCopiesNestedFields(t *testing.T) {
copied.Spec.RemoteStorage.LMCacheServer.Resources == nil {
t.Fatalf("remoteStorage.lmCacheServer nested fields were not deep-copied")
}
if copied.Spec.RemoteStorage.NFS == nil || copied.Spec.RemoteStorage.NFS.Server != "10.0.0.25" {
t.Fatalf("copied NFS storage = %+v, want independent original values", copied.Spec.RemoteStorage.NFS)
}
copiedProviderMemory := copied.Spec.RemoteStorage.LMCacheServer.Resources.Limits[corev1.ResourceMemory]
if copiedProviderMemory.Cmp(resource.MustParse("2Gi")) != 0 {
t.Fatalf("remoteStorage.lmCacheServer resources were not deep-copied")
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 17 additions & 10 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/cachebox-project/inference-cache/internal/controller"
podwebhook "github.com/cachebox-project/inference-cache/internal/webhook/pod"
cachewebhookv1alpha1 "github.com/cachebox-project/inference-cache/internal/webhook/v1alpha1"
backendprovider "github.com/cachebox-project/inference-cache/pkg/adapters/backend/provider"
adapterruntime "github.com/cachebox-project/inference-cache/pkg/adapters/runtime"
externaladapter "github.com/cachebox-project/inference-cache/pkg/adapters/runtime/external"
sglangadapter "github.com/cachebox-project/inference-cache/pkg/adapters/runtime/sglang"
Expand Down Expand Up @@ -160,6 +161,10 @@ func main() {
adapterruntime.WithSubscriberImage(opts.subscriberImage),
adapterruntime.WithPolicyServerGRPCAddress(opts.policyServerGRPCAddress),
))
// Keep the controller and pod webhook on the same remote-provider
// capability registry, just as they share the runtime-adapter registry.
// Today it contains External NFS but no Managed NFS implementation.
backendRegistry := backendprovider.DefaultRegistry()

// /probe wrapper for the CacheBackend reconciler's functional-probe gate.
// An empty ProbeURL disables the gate — useful for local-dev runs that
Expand All @@ -169,13 +174,14 @@ func main() {
probeClient := &controller.ProbeClient{ProbeURL: opts.serverProbeURL}

if err := (&controller.CacheBackendReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("CacheBackend"),
Recorder: mgr.GetEventRecorder("cachebackend-controller"),
APIReader: mgr.GetAPIReader(),
Registry: adapterRegistry,
ProbeClient: probeClient,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("CacheBackend"),
Recorder: mgr.GetEventRecorder("cachebackend-controller"),
APIReader: mgr.GetAPIReader(),
Registry: adapterRegistry,
BackendRegistry: backendRegistry,
ProbeClient: probeClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CacheBackend")
os.Exit(1)
Expand Down Expand Up @@ -241,9 +247,10 @@ func main() {
// avoid a cold-cache window on controller startup.
mgr.GetWebhookServer().Register(podwebhook.WebhookPath, &webhook.Admission{
Handler: &podwebhook.EngineInjector{
Reader: mgr.GetAPIReader(),
Registry: adapterRegistry,
Log: ctrl.Log.WithName("webhooks").WithName("pod-injector"),
Reader: mgr.GetAPIReader(),
Registry: adapterRegistry,
BackendRegistry: backendRegistry,
Log: ctrl.Log.WithName("webhooks").WithName("pod-injector"),
},
})

Expand Down
61 changes: 50 additions & 11 deletions config/crd/bases/inferencecache.io_cachebackends.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ spec:
format: int32
minimum: 1
type: integer
storagePrefetchPolicy:
description: |-
StoragePrefetchPolicy maps to --hicache-storage-prefetch-policy when
remoteStorage.provider=NFS. It is rejected without that storage tier.
enum:
- best_effort
- wait_complete
- timeout
type: string
writePolicy:
description: WritePolicy maps to --hicache-write-policy.
enum:
Expand Down Expand Up @@ -523,21 +532,23 @@ spec:
default: true
description: |-
FailOpen controls whether the engine treats cache lookups as a soft
dependency. When true (the default), an unreachable or degraded cache
backend MUST fall back to local prefill and never fail a serving
request — the cache is an optimization, not a serving dependency. When
explicitly set to false the engine fails requests on cache
unreachability ("fail-closed"); the cache becomes a serving
dependency, which is loud and visible via a Warning Event on the
owning CacheBackend.
dependency. For integrations admitted with true (the default), an
unreachable or degraded cache backend MUST fall back to local prefill
and never fail a serving request — the cache is an optimization, not a
serving dependency. When explicitly set to false the cache becomes a
fail-closed serving dependency, which is loud and visible via a Warning
Event on the owning CacheBackend.

The flag is plumbed by the engine adapter as INFERENCECACHE_FAIL_OPEN
(both shipping LMCache adapters — vLLM+LMCache and SGLang+LMCache —
inject it). Per-request fail-open enforcement at the engine level is the
engine/connector's responsibility; the cache plane surfaces the bit so
the engine can honor it.
SGLangHiCache accepts only the default true value and does not inject this
env var because native HiCache exposes no equivalent fail-closed control.
Host-only SGLangHiCache accepts the default true value and does not inject
this env var because native HiCache exposes no equivalent control.
NFS-backed SGLangHiCache instead requires an explicit false value: kubelet
mounts the inline NFS volume before containers start, so export
availability is inherently a serving dependency.
type: boolean
firstEventTimeout:
default: 5m
Expand Down Expand Up @@ -693,8 +704,9 @@ spec:
properties:
endpoint:
description: |-
Endpoint is required for External ownership and rejected for Managed
ownership, whose endpoint is controller-observed.
Endpoint is required for network providers with External ownership and
rejected for Managed ownership, whose endpoint is controller-observed.
NFS is mounted from its typed server/path fields and has no endpoint.
type: string
lmCacheServer:
description: LMCacheServer contains standalone lmcache-server-owned
Expand Down Expand Up @@ -847,6 +859,32 @@ spec:
type: object
type: object
type: object
nfs:
description: |-
NFS identifies an externally owned NFS export and its engine-container
mount path. It is valid only with provider=NFS and ownership=External.
properties:
mountPath:
description: |-
MountPath is the absolute path at which the export is mounted in the
engine container.
minLength: 1
type: string
path:
description: Path is the absolute path exported by the NFS
server.
minLength: 1
type: string
server:
description: Server is the NFS mount-target hostname or IP
address.
minLength: 1
type: string
required:
- mountPath
- path
- server
type: object
ownership:
description: |-
Ownership identifies whether inference-cache manages the provider
Expand All @@ -861,6 +899,7 @@ spec:
- Redis
- LMCacheServer
- Mooncake
- NFS
type: string
redis:
description: Redis contains Redis-owned configuration.
Expand Down
Loading
Loading