Skip to content

Commit 9014b0a

Browse files
feat(epp): wire in-flight eviction behind flowControl.enableEviction
The confirmation grace is derived at wiring time from the selected saturation detector (dispatch-tick scale for the concurrency detector; refresh interval + staleness + engine reclaim budget for scraped sensors); remaining controller parameters are internal. The Director tracks dispatches and stream termination; the ext_proc server receives the evict-channel lookup. Signed-off-by: Luke Van Drie <lukevandrie@google.com> Co-authored-by: Rishabh Saini <rishabhsaini01@gmail.com> Signed-off-by: Luke Van Drie <lukevandrie@google.com>
1 parent 792e774 commit 9014b0a

3 files changed

Lines changed: 121 additions & 15 deletions

File tree

cmd/epp/runner/runner.go

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ import (
5656
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol"
5757
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/contracts"
5858
fccontroller "github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/controller"
59+
fceviction "github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/eviction"
5960
fcregistry "github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/registry"
6061
fwkdl "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/datalayer"
62+
fwkfc "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/flowcontrol"
6163
fwkplugin "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/plugin"
6264
attrconcurrency "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/concurrency"
6365
attrlatency "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/latency"
@@ -70,6 +72,8 @@ import (
7072
sourcemetrics "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/metrics"
7173
srcmodels "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/models"
7274
sourcenotifications "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/notifications"
75+
evictfiltering "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/eviction/filtering"
76+
evictordering "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/eviction/ordering"
7377
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/fairness/globalstrict"
7478
programaware "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/fairness/program-aware"
7579
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/flowcontrol/fairness/roundrobin"
@@ -448,9 +452,12 @@ func (r *Runner) setup(ctx context.Context, cfg *rest.Config, opts *runserver.Op
448452

449453
endpointCandidates := contracts.EndpointCandidates(requestcontrol.NewDatastoreEndpointCandidates(ds,
450454
requestcontrol.WithDisableEndpointSubsetFilter(opts.DisableEndpointSubsetFilter)))
451-
endpointCandidates, admissionController, priorityBandControlPlane := r.initAdmissionControl(ctx, opts, eppConfig, endpointCandidates)
455+
endpointCandidates, admissionController, priorityBandControlPlane, requestEvictor := r.initAdmissionControl(ctx, opts, eppConfig, endpointCandidates)
452456

453457
director := requestcontrol.NewDirectorWithConfig(ds, scheduler, admissionController, endpointCandidates, r.requestControlConfig)
458+
if requestEvictor != nil {
459+
director.SetRequestEvictor(requestEvictor)
460+
}
454461

455462
serverRunner := &runserver.ExtProcServerRunner{
456463
GrpcPort: opts.GRPCPort,
@@ -471,6 +478,9 @@ func (r *Runner) setup(ctx context.Context, cfg *rest.Config, opts *runserver.Op
471478
GRPCMaxSendMsgSize: opts.GRPCMaxSendMsgSize,
472479
EnableGRPCStreamMetrics: opts.EnableGRPCStreamMetrics,
473480
}
481+
if requestEvictor != nil {
482+
serverRunner.EvictChannelLookup = requestEvictor.EvictionRegistry()
483+
}
474484

475485
if err := serverRunner.SetupWithManager(mgr); err != nil {
476486
setupLog.Error(err, "Failed to setup EPP controllers")
@@ -876,28 +886,87 @@ func (r *Runner) initAdmissionControl(
876886
opts *runserver.Options,
877887
eppConfig *config.Config,
878888
endpointCandidates contracts.EndpointCandidates,
879-
) (contracts.EndpointCandidates, requestcontrol.AdmissionController, contracts.PriorityBandControlPlane) {
889+
) (contracts.EndpointCandidates, requestcontrol.AdmissionController, contracts.PriorityBandControlPlane, *fceviction.RequestEvictor) {
880890
if !r.featureGates[flowcontrol.FeatureGate] {
881891
setupLog.Info("Experimental Flow Control layer is disabled, using legacy admission control")
882892
return endpointCandidates,
883893
requestcontrol.NewLegacyAdmissionController(eppConfig.SaturationDetector, endpointCandidates),
894+
nil,
884895
nil
885896
}
886897
endpointCandidates = requestcontrol.NewCachedEndpointCandidates(ctx, endpointCandidates, 50*time.Millisecond)
887898
setupLog.Info("Initializing experimental Flow Control layer")
888899
registry := fcregistry.NewFlowRegistry(eppConfig.FlowControlConfig.Registry, setupLog)
889-
fc := fccontroller.NewFlowController(
890-
ctx,
891-
opts.PoolName,
892-
eppConfig.FlowControlConfig.Controller,
893-
fccontroller.Deps{
894-
Registry: registry,
895-
SaturationDetector: eppConfig.SaturationDetector,
896-
EndpointCandidates: endpointCandidates,
897-
UsageLimitPolicy: eppConfig.FlowControlConfig.UsageLimitPolicy,
898-
},
899-
)
900-
return endpointCandidates, requestcontrol.NewFlowControlAdmissionController(fc, opts.PoolName), registry
900+
901+
deps := fccontroller.Deps{
902+
Registry: registry,
903+
SaturationDetector: eppConfig.SaturationDetector,
904+
EndpointCandidates: endpointCandidates,
905+
UsageLimitPolicy: eppConfig.FlowControlConfig.UsageLimitPolicy,
906+
}
907+
908+
var requestEvictor *fceviction.RequestEvictor
909+
if eppConfig.FlowControlConfig.Controller.EnableEviction {
910+
var err error
911+
requestEvictor, err = buildRequestEvictor()
912+
if err != nil {
913+
setupLog.Error(err, "Failed to build eviction plumbing; in-flight eviction disabled")
914+
} else {
915+
deps.InFlightEvictor = requestEvictor
916+
grace := deriveEvictionConfirmationGrace(eppConfig.SaturationDetector, opts)
917+
eppConfig.FlowControlConfig.Controller.EvictionConfirmationGrace = grace
918+
setupLog.Info("In-flight eviction plumbing initialized",
919+
"filter", evictfiltering.SheddableFilterType,
920+
"ordering", evictordering.PriorityThenTimeOrderingType,
921+
"confirmationGrace", grace)
922+
}
923+
}
924+
925+
fc := fccontroller.NewFlowController(ctx, opts.PoolName, eppConfig.FlowControlConfig.Controller, deps)
926+
return endpointCandidates, requestcontrol.NewFlowControlAdmissionController(fc, opts.PoolName), registry, requestEvictor
927+
}
928+
929+
const (
930+
// evictionEngineReclaimBudget covers the engine-side abort and KV garbage-collection time
931+
// between a revoked request's stream termination and the freed capacity appearing in scraped
932+
// metrics.
933+
evictionEngineReclaimBudget = 250 * time.Millisecond
934+
// evictionLeadingSensorGrace covers scheduling jitter for sensors whose gauge updates in the
935+
// same event chain as the confirmation (a few dispatch ticks).
936+
evictionLeadingSensorGrace = 10 * time.Millisecond
937+
)
938+
939+
// deriveEvictionConfirmationGrace computes the reclamation pacing grace from the paired saturation
940+
// detector's confirmation-to-visibility lag. The grace is a property of the sensor, not a
941+
// preference, so it is derived rather than configured (see docs/flow-control-eviction.md).
942+
func deriveEvictionConfirmationGrace(detector fwkfc.SaturationDetector, opts *runserver.Options) time.Duration {
943+
if detector != nil && detector.TypedName().Type == concurrency.ConcurrencyDetectorType {
944+
// The concurrency detector's counters decrement when the stream terminates: the gauge leads
945+
// physical reclamation, and only dispatch-loop jitter separates confirmation from visibility.
946+
return evictionLeadingSensorGrace
947+
}
948+
// Scraped sensors (utilization detector, or any custom detector, conservatively): the freed
949+
// capacity is visible only after the engine reclaims it and the next non-stale scrape lands.
950+
return opts.RefreshMetricsInterval + opts.MetricsStalenessThreshold + evictionEngineReclaimBudget
951+
}
952+
953+
// buildRequestEvictor assembles the in-flight eviction plumbing: the victim filter and ordering
954+
// policies, the ImmediateResponse eviction mechanism, and the tracking queue that ties them
955+
// together. See docs/flow-control-eviction.md.
956+
func buildRequestEvictor() (*fceviction.RequestEvictor, error) {
957+
orderingPlugin, err := evictordering.PriorityThenTimeOrderingFactory(evictordering.PriorityThenTimeOrderingType, nil, nil)
958+
if err != nil {
959+
return nil, fmt.Errorf("failed to create eviction ordering policy: %w", err)
960+
}
961+
filterPlugin, err := evictfiltering.SheddableFilterFactory(evictfiltering.SheddableFilterType, nil, nil)
962+
if err != nil {
963+
return nil, fmt.Errorf("failed to create eviction filter policy: %w", err)
964+
}
965+
return fceviction.NewRequestEvictor(
966+
orderingPlugin.(fwkfc.EvictionOrderingPolicy),
967+
filterPlugin.(fwkfc.EvictionFilterPolicy),
968+
fceviction.NewImmediateResponseEvictor(),
969+
), nil
901970
}
902971

903972
// runWithFileDiscovery handles the execution path when a discovery plugin is configured.
@@ -974,8 +1043,11 @@ func (r *Runner) runWithFileDiscovery(ctx context.Context, opts *runserver.Optio
9741043
requestcontrol.WithDisableEndpointSubsetFilter(opts.DisableEndpointSubsetFilter)))
9751044
// File-discovery mode has no InferenceObjective reconciler to drive the
9761045
// control plane; static bands from config apply at registry construction.
977-
endpointCandidates, admissionController, _ := r.initAdmissionControl(ctx, opts, eppConfig, endpointCandidates)
1046+
endpointCandidates, admissionController, _, requestEvictor := r.initAdmissionControl(ctx, opts, eppConfig, endpointCandidates)
9781047
director := requestcontrol.NewDirectorWithConfig(ds, scheduler, admissionController, endpointCandidates, r.requestControlConfig)
1048+
if requestEvictor != nil {
1049+
director.SetRequestEvictor(requestEvictor)
1050+
}
9791051

9801052
gknn := common.GKNN{
9811053
NamespacedName: types.NamespacedName{Name: poolName, Namespace: namespace},
@@ -998,6 +1070,9 @@ func (r *Runner) runWithFileDiscovery(ctx context.Context, opts *runserver.Optio
9981070
GRPCMaxSendMsgSize: opts.GRPCMaxSendMsgSize,
9991071
EnableGRPCStreamMetrics: opts.EnableGRPCStreamMetrics,
10001072
}
1073+
if requestEvictor != nil {
1074+
serverRunner.EvictChannelLookup = requestEvictor.EvictionRegistry()
1075+
}
10011076

10021077
r.customCollectors = append(r.customCollectors, collectors.NewInferencePoolMetricsCollector(ds))
10031078
metrics.Register(r.customCollectors...)

pkg/epp/requestcontrol/director.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/llm-d/llm-d-router/pkg/epp/datalayer"
4444
"github.com/llm-d/llm-d-router/pkg/epp/datastore"
4545
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/contracts"
46+
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/eviction"
4647
fwkdl "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/datalayer"
4748
fwkrc "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/requestcontrol"
4849
fwkrh "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/requesthandling"
@@ -210,6 +211,10 @@ type Director struct {
210211
// streaming response path. The request context key avoids coupling independent streams that reuse the same
211212
// x-request-id header.
212213
responseBodyQueues sync.Map
214+
215+
// requestEvictor, when set, tracks dispatched requests for demand-driven in-flight eviction.
216+
// See docs/flow-control-eviction.md.
217+
requestEvictor *eviction.RequestEvictor
213218
}
214219

215220
// getInferenceObjective fetches the inferenceObjective from the datastore otherwise creates a new one based on reqCtx.
@@ -483,9 +488,19 @@ func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestC
483488

484489
d.runPreRequestPlugins(ctx, reqCtx.SchedulingRequest, result)
485490

491+
if d.requestEvictor != nil {
492+
d.requestEvictor.PreRequest(ctx, reqCtx.SchedulingRequest, result)
493+
}
494+
486495
return reqCtx, nil
487496
}
488497

498+
// SetRequestEvictor wires the in-flight eviction tracker into the request lifecycle.
499+
// Must be called before the Director serves traffic.
500+
func (d *Director) SetRequestEvictor(re *eviction.RequestEvictor) {
501+
d.requestEvictor = re
502+
}
503+
489504
func (d *Director) toSchedulerEndpoints(endpoints []fwkdl.Endpoint) []fwksched.Endpoint {
490505
result := make([]fwksched.Endpoint, len(endpoints))
491506
for i, endpoint := range endpoints {
@@ -522,6 +537,16 @@ func (d *Director) HandleResponseHeader(ctx context.Context, reqCtx *handlers.Re
522537
func (d *Director) HandleResponseBody(ctx context.Context, reqCtx *handlers.RequestContext, endOfStream bool) *handlers.RequestContext {
523538
logger := log.FromContext(ctx).WithValues("stage", "bodyChunk")
524539
logger.V(logutil.TRACE).Info("Entering HandleResponseBodyChunk")
540+
541+
// The eviction tracker must observe stream termination even when no streaming plugins are
542+
// registered, so this runs before the early return below.
543+
if endOfStream && d.requestEvictor != nil {
544+
d.requestEvictor.ResponseBody(ctx, reqCtx.SchedulingRequest, &fwkrc.Response{
545+
RequestID: reqCtx.Request.Headers[reqcommon.RequestIDHeaderKey],
546+
EndOfStream: true,
547+
}, reqCtx.TargetPod)
548+
}
549+
525550
if len(d.requestControlPlugins.responseStreamingPlugins) == 0 {
526551
logger.V(logutil.TRACE).Info("Exiting HandleResponseBodyChunk")
527552
return reqCtx

pkg/epp/server/runserver.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ type ExtProcServerRunner struct {
6666
GRPCMaxRecvMsgSize int
6767
GRPCMaxSendMsgSize int
6868
EnableGRPCStreamMetrics bool
69+
// EvictChannelLookup, when set, enables the ext_proc server to terminate in-flight requests
70+
// selected for eviction by flow control. See docs/flow-control-eviction.md.
71+
EvictChannelLookup handlers.EvictChannelLookup
6972
}
7073

7174
// NewDefaultExtProcServerRunner creates a runner with default values.
@@ -214,6 +217,9 @@ func (r *ExtProcServerRunner) AsRunnable(logger logr.Logger) manager.Runnable {
214217
poolCap = 4 * 1024 * 1024 // gRPC default 4MB
215218
}
216219
extProcServer := handlers.NewStreamingServer(r.Datastore, r.Director, r.ParserRegistry, poolCap)
220+
if r.EvictChannelLookup != nil {
221+
extProcServer.SetEvictChannelLookup(r.EvictChannelLookup)
222+
}
217223
extProcPb.RegisterExternalProcessorServer(srv, extProcServer)
218224

219225
if r.HealthChecking {

0 commit comments

Comments
 (0)