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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ generate: controller-gen ## Generate Kubernetes deepcopy code.

.PHONY: manifests
manifests: controller-gen ## Generate CRD, RBAC, and webhook manifests.
$(CONTROLLER_GEN) rbac:roleName=inference-cache-manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases output:rbac:artifacts:config=config/rbac output:webhook:artifacts:config=config/webhook
# CachePolicy.rankerOverrides intentionally exposes bounded float32 fields.
$(CONTROLLER_GEN) rbac:roleName=inference-cache-manager-role crd:allowDangerousTypes=true webhook paths="./..." output:crd:artifacts:config=config/crd/bases output:rbac:artifacts:config=config/rbac output:webhook:artifacts:config=config/webhook

.PHONY: proto-gen
proto-gen: protoc-gen-go ## Generate protobuf Go code.
Expand Down
50 changes: 46 additions & 4 deletions api/v1alpha1/cachepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,11 @@ type CachePolicySpec struct {
// affinityRouting: Disabled. An operator can disable either floor by
// setting it to its opt-out value (0 / "0").
//
// Encoded as a stringified float to avoid introducing the first
// float-typed field into the CachePolicy schema (the others are
// int32 / duration). Validated by the +kubebuilder:validation:Pattern
// marker: digits with an optional decimal part, no sign. The pattern
// Encoded as a stringified float for backward compatibility with the
// original v1alpha1 field and its string-based clients. Unlike the bounded
// numeric fields under rankerOverrides, this legacy threshold is validated
// by the +kubebuilder:validation:Pattern marker: digits with an optional
// decimal part, no sign. The pattern
// caps the integer part at 8 digits (up to 99,999,999) and the
// decimal part at 6 digits — well inside float32 representable
// range (~3.4e38), so a value the apiserver admits CANNOT overflow
Expand All @@ -174,6 +175,12 @@ type CachePolicySpec struct {
// +kubebuilder:validation:Minimum=0
LookupTimeoutMs *int32 `json:"lookupTimeoutMs,omitempty"`

// RankerOverrides tunes ranking-v2 for this namespace. Each omitted field
// inherits the server's RankerConfig baseline; explicit zero values retain
// their documented kill-switch behavior.
// +optional
RankerOverrides *CachePolicyRankerOverridesSpec `json:"rankerOverrides,omitempty"`

// Strategy controls which LookupRoute matching strategies may produce a
// hint for this namespace. Defaults preserve the historical behavior:
// longest-prefix chain matching is enabled, callers are not required to
Expand Down Expand Up @@ -208,6 +215,41 @@ type CachePolicySpec struct {
AffinityRouting *CachePolicyAffinityRouting `json:"affinityRouting,omitempty"`
}

// CachePolicyRankerOverridesSpec carries presence-aware per-namespace
// overrides for the ranking-v2 pressure, SLO, and TENANT_HOT knobs.
type CachePolicyRankerOverridesSpec struct {
// PressureWeight controls the replica-pressure penalty. Zero disables it.
// +optional
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=4
PressureWeight *float32 `json:"pressureWeight,omitempty"`

// SLOTightTTFTMs is the TTFT threshold below which SLOTightBias applies.
// Zero disables the SLO bias threshold.
// +optional
// +kubebuilder:validation:Minimum=0
SLOTightTTFTMs *int32 `json:"sloTightTTFTMs,omitempty"`

// SLOTightBias controls the freshness boost for tight-TTFT requests. Zero
// disables the boost.
// +optional
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=8
SLOTightBias *float32 `json:"sloTightBias,omitempty"`

// TenantHotMinHitRate is the minimum reported hit rate for TENANT_HOT.
// +optional
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=1
TenantHotMinHitRate *float32 `json:"tenantHotMinHitRate,omitempty"`

// TenantHotMaxAge is the maximum age of replica stats eligible for
// TENANT_HOT. Zero disables the fallback; negative durations are rejected
// by the validating webhook.
// +optional
TenantHotMaxAge *metav1.Duration `json:"tenantHotMaxAge,omitempty"`
}

// CachePolicyStrategySpec controls per-namespace LookupRoute strategy gates.
type CachePolicyStrategySpec struct {
// EnableChainMatching allows LookupRoute requests that carry block_hashes
Expand Down
32 changes: 32 additions & 0 deletions api/v1alpha1/remaining_crds_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ func TestRemainingCRDSchemas(t *testing.T) {
requireDurationLike(t, mustProperty(t, policySpec, "evictionTTL"))
requireMinimum(t, mustProperty(t, policySpec, "minimumPrefixTokens"), 0)
requireMinimum(t, mustProperty(t, policySpec, "lookupTimeoutMs"), 0)
rankerOverrides := mustProperty(t, policySpec, "rankerOverrides")
requireMinimum(t, mustProperty(t, rankerOverrides, "pressureWeight"), 0)
requireMaximum(t, mustProperty(t, rankerOverrides, "pressureWeight"), 4)
requireMinimum(t, mustProperty(t, rankerOverrides, "sloTightTTFTMs"), 0)
requireMinimum(t, mustProperty(t, rankerOverrides, "sloTightBias"), 0)
requireMaximum(t, mustProperty(t, rankerOverrides, "sloTightBias"), 8)
requireMinimum(t, mustProperty(t, rankerOverrides, "tenantHotMinHitRate"), 0)
requireMaximum(t, mustProperty(t, rankerOverrides, "tenantHotMinHitRate"), 1)
requireDurationLike(t, mustProperty(t, rankerOverrides, "tenantHotMaxAge"))
policyStrategy := mustProperty(t, policySpec, "strategy")
requireDefault(t, mustProperty(t, policyStrategy, "enableChainMatching"), true)
requireDefault(t, mustProperty(t, policyStrategy, "requireChain"), false)
Expand Down Expand Up @@ -92,12 +101,24 @@ func TestRemainingCRDDeepCopies(t *testing.T) {
enableChainMatching := true
requireChain := false
enableTenantHot := true
pressureWeight := float32(1)
sloTightTTFTMs := int32(200)
sloTightBias := float32(1)
tenantHotMinHitRate := float32(0.1)
tenantHotMaxAge := metav1.Duration{Duration: 5 * time.Minute}
policy := &CachePolicy{
Spec: CachePolicySpec{
Eviction: CachePolicyEvictionAlgorithmLRU,
EvictionTTL: &ttl,
MinimumPrefixTokens: &minimumPrefixTokens,
LookupTimeoutMs: &lookupTimeoutMs,
RankerOverrides: &CachePolicyRankerOverridesSpec{
PressureWeight: &pressureWeight,
SLOTightTTFTMs: &sloTightTTFTMs,
SLOTightBias: &sloTightBias,
TenantHotMinHitRate: &tenantHotMinHitRate,
TenantHotMaxAge: &tenantHotMaxAge,
},
Strategy: &CachePolicyStrategySpec{
EnableChainMatching: &enableChainMatching,
RequireChain: &requireChain,
Expand All @@ -111,13 +132,24 @@ func TestRemainingCRDDeepCopies(t *testing.T) {
*policy.Spec.Strategy.EnableChainMatching = false
*policy.Spec.Strategy.RequireChain = true
*policy.Spec.Strategy.EnableTenantHot = false
*policy.Spec.RankerOverrides.PressureWeight = 2
*policy.Spec.RankerOverrides.SLOTightTTFTMs = 100
*policy.Spec.RankerOverrides.SLOTightBias = 3
*policy.Spec.RankerOverrides.TenantHotMinHitRate = 0.5
policy.Spec.RankerOverrides.TenantHotMaxAge.Duration = time.Minute
policy.Status.Conditions[0].Message = "changed"
if policyCopy.Spec.Eviction != CachePolicyEvictionAlgorithmLRU ||
policyCopy.Spec.EvictionTTL.Duration != time.Minute ||
policyCopy.Spec.Strategy == nil ||
policyCopy.Spec.Strategy.EnableChainMatching == nil || !*policyCopy.Spec.Strategy.EnableChainMatching ||
policyCopy.Spec.Strategy.RequireChain == nil || *policyCopy.Spec.Strategy.RequireChain ||
policyCopy.Spec.Strategy.EnableTenantHot == nil || !*policyCopy.Spec.Strategy.EnableTenantHot ||
policyCopy.Spec.RankerOverrides == nil ||
policyCopy.Spec.RankerOverrides.PressureWeight == nil || *policyCopy.Spec.RankerOverrides.PressureWeight != 1 ||
policyCopy.Spec.RankerOverrides.SLOTightTTFTMs == nil || *policyCopy.Spec.RankerOverrides.SLOTightTTFTMs != 200 ||
policyCopy.Spec.RankerOverrides.SLOTightBias == nil || *policyCopy.Spec.RankerOverrides.SLOTightBias != 1 ||
policyCopy.Spec.RankerOverrides.TenantHotMinHitRate == nil || *policyCopy.Spec.RankerOverrides.TenantHotMinHitRate != 0.1 ||
policyCopy.Spec.RankerOverrides.TenantHotMaxAge == nil || policyCopy.Spec.RankerOverrides.TenantHotMaxAge.Duration != 5*time.Minute ||
policyCopy.Status.Conditions[0].Message != "ok" {
t.Fatalf("CachePolicy was not deep-copied")
}
Expand Down
45 changes: 45 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.

48 changes: 44 additions & 4 deletions config/crd/bases/inferencecache.io_cachepolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,45 @@ spec:
format: int32
minimum: 0
type: integer
rankerOverrides:
description: |-
RankerOverrides tunes ranking-v2 for this namespace. Each omitted field
inherits the server's RankerConfig baseline; explicit zero values retain
their documented kill-switch behavior.
properties:
pressureWeight:
description: PressureWeight controls the replica-pressure penalty.
Zero disables it.
maximum: 4
minimum: 0
type: number
sloTightBias:
description: |-
SLOTightBias controls the freshness boost for tight-TTFT requests. Zero
disables the boost.
maximum: 8
minimum: 0
type: number
sloTightTTFTMs:
description: |-
SLOTightTTFTMs is the TTFT threshold below which SLOTightBias applies.
Zero disables the SLO bias threshold.
format: int32
minimum: 0
type: integer
tenantHotMaxAge:
description: |-
TenantHotMaxAge is the maximum age of replica stats eligible for
TENANT_HOT. Zero disables the fallback; negative durations are rejected
by the validating webhook.
type: string
tenantHotMinHitRate:
description: TenantHotMinHitRate is the minimum reported hit rate
for TENANT_HOT.
maximum: 1
minimum: 0
type: number
type: object
routingFloorScore:
default: "0.1"
description: |-
Expand Down Expand Up @@ -176,10 +215,11 @@ spec:
affinityRouting: Disabled. An operator can disable either floor by
setting it to its opt-out value (0 / "0").

Encoded as a stringified float to avoid introducing the first
float-typed field into the CachePolicy schema (the others are
int32 / duration). Validated by the +kubebuilder:validation:Pattern
marker: digits with an optional decimal part, no sign. The pattern
Encoded as a stringified float for backward compatibility with the
original v1alpha1 field and its string-based clients. Unlike the bounded
numeric fields under rankerOverrides, this legacy threshold is validated
by the +kubebuilder:validation:Pattern marker: digits with an optional
decimal part, no sign. The pattern
caps the integer part at 8 digits (up to 99,999,999) and the
decimal part at 6 digits — well inside float32 representable
range (~3.4e38), so a value the apiserver admits CANNOT overflow
Expand Down
10 changes: 10 additions & 0 deletions config/samples/cache_v1alpha1_cachepolicy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ spec:
# survivor).
routingFloorScore: "0.1"
lookupTimeoutMs: 20
rankerOverrides:
# Per-namespace overrides are optional and merge onto the server baseline.
# pressureWeight=2 is intentionally observable in the install smoke: a
# replica reporting pressure=0.5 is fully demoted (factor 0), proving the
# controller -> /policy -> PolicyStore -> index path adopted this object.
pressureWeight: 2
sloTightTTFTMs: 150
sloTightBias: 2
tenantHotMinHitRate: 0.25
tenantHotMaxAge: 2m
strategy:
# Enable longest-common-prefix block-hash chain matching when callers
# send block_hashes + block_token_counts. Disable to force legacy exact
Expand Down
9 changes: 8 additions & 1 deletion docs/concepts/cachepolicy-tuning.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ snapshot. The CR is purely declarative: the controller pushes its resolved field
server, which is where enforcement actually happens (see
[Propagation](#propagation-controller--server) below).

## The seven spec knobs
## The nine spec knobs

| Field | Type | Default | When to tune |
|---|---|---|---|
Expand All @@ -36,6 +36,7 @@ server, which is where enforcement actually happens (see
| `routingFloorScore` | stringified float, e.g. `"0.1"`, `"5"`, `"0"` | `"0.1"` | Per-replica *score* floor below which a `PREFIX_MATCH` response is downgraded off the prefix-match path. Applied AFTER the lookup runs, against the per-replica score from the distinguishing-power-aware ranker (`matched_tokens × freshness × pressure_factor × slo_bias × distinguishing_power`). Overlaps held by every replica (chat-template framing, RAG corpus headers, custom system prompts shared across the deployment) produce `distinguishing_power = 0` and score = 0 — this floor catches them. The downgrade lands on `StrategyNone`, which surfaces as `AFFINITY_HINT` (default-enabled affinity) or `NO_HINT` (affinity disabled). Composes with `minimumMatchedTokens` — the matched-tokens floor runs first (per-replica), then this score floor gates the top survivor. Set to `"0"` to disable entirely (raw-recall benchmarking / debug). |
| `affinityRouting` | enum `Enabled` \| `Disabled` | `Enabled` | Toggles the consistent-hash fallback on the `StrategyNone` branch. With `Enabled` (the default), any `StrategyNone` result with a usable seed + at least one replica in `servingByScope[(tenant, model, hash_scheme)]` surfaces as `AFFINITY_HINT` with a stable single-replica pick (`SHA-256(canonical_seed) mod len(sorted servingByScope)`) — repeat prompts pin to the same replica and warm T1 on diffuse single-turn workloads. With `Disabled`, the same response stays on `NO_HINT` and the gateway round-robins; useful for raw-recall benchmarking and ranker debugging. Diagnostic codes (`UNKNOWN_*`) and `TIMEOUT` keep precedence over `AFFINITY_HINT`. |
| `lookupTimeoutMs` | int32 (min `0`) | unset = no deadline | Per-lookup latency budget in milliseconds. A breach returns reason code `TIMEOUT` (still fail-open — empty result, never an error to the gateway). See the foot-gun in [Gotchas](#two-gotchas). |
| `rankerOverrides` | object | server `RankerConfig` baseline | Optional per-namespace overlay for `pressureWeight` (`0..4`), `sloTightTTFTMs` (`>=0`), `sloTightBias` (`0..8`), `tenantHotMinHitRate` (`0..1`), and `tenantHotMaxAge` (non-negative duration). Omitted nested fields inherit the server baseline; explicit zero values retain their kill-switch meaning. |
| `strategy` | object | chain matching on, chain not required, tenant-hot on | Per-namespace LookupRoute strategy gates. Use `enableChainMatching: false` to force exact `prefix_hash` matching, `requireChain: true` to reject non-chain callers with `POLICY_REQUIRES_CHAIN`, or `enableTenantHot: false` to suppress soft tenant-hot hints. |

`status` carries only `observedGeneration` + `conditions`, and both are **reserved** — the
Expand All @@ -61,6 +62,12 @@ spec:
routingFloorScore: "0.1" # downgrade off PREFIX_MATCH when top score is below the floor — surfaces as AFFINITY_HINT under default-enabled affinity or NO_HINT when disabled (result-side score floor, default "0.1")
affinityRouting: Enabled # consistent-hash fallback on the StrategyNone branch; set Disabled for raw-recall benchmarking / debug
lookupTimeoutMs: 20 # positive => a real 20ms deadline (NOT 0 — see Gotchas)
rankerOverrides: # optional; omitted fields keep the server baseline
pressureWeight: 1
sloTightTTFTMs: 200
sloTightBias: 1
tenantHotMinHitRate: 0.1
tenantHotMaxAge: 5m
strategy:
enableChainMatching: true # default: use block-hash longest-prefix matching when callers send chains
requireChain: false # default: legacy exact prefix_hash callers still work
Expand Down
15 changes: 15 additions & 0 deletions docs/design/lookuproute-ranking.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,21 @@ Every factor and threshold is tunable through a `RankerConfig` (in-binary
defaults) or a `CachePolicy` CR (per-namespace overrides). Defaults are
set so that:

| `CachePolicy.spec.rankerOverrides` field | `RankerConfig` field | Valid range | Inherited default |
|---|---|---|---|
| `pressureWeight` | `PressureWeight` | `0..4` | `1.0` |
| `sloTightTTFTMs` | `SLOTightTTFTMs` | `>= 0` | `200` |
| `sloTightBias` | `SLOTightBias` | `0..8` | `1.0` |
| `tenantHotMinHitRate` | `TenantHotMinHitRate` | `0..1` | `0.1` |
| `tenantHotMaxAge` | `TenantHotMaxAge` | duration `>= 0` | `5m` |

The object and all five fields are optional. The index resolves one effective
configuration per lookup by copying its server-wide `WithRanker` baseline and
overlaying only non-nil fields. This keeps an omitted field on the calibrated
baseline while preserving explicit zero as a real kill switch. A missing
policy, a missing `rankerOverrides` object, or a deleted policy all use the
server baseline.

- A deployment with **no stats reported** sees pressure factor 1 and no
`TENANT_HOT` candidates qualify — those two strategies collapse to the
pre-PR baseline. The §2.6 matched-tokens floor and the §2.7
Expand Down
Loading
Loading