-
Notifications
You must be signed in to change notification settings - Fork 353
feat(flowcontrol): support scoped request queue TTLs #2649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
fa9da56
e0db7a7
763f151
352081b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,7 +64,9 @@ Tuning knobs, all under the `flowControl:` config section: | |
| behavior for sheddable traffic). | ||
| * `defaultRequestTTL` — the queue-wait budget against a pool that has endpoints, and the other way a | ||
| request is shed. Keep it under the client or gateway deadline, and size it to the time-to-first-token | ||
| budget you are willing to spend waiting on a saturated pool. | ||
| budget you are willing to spend waiting on a saturated pool. Priority-band entries and templates | ||
| may replace the global value, including with `0s` for unbounded queue wait. Clients may shorten the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same scope point for the operator-facing doc. The header shortens the saturation budget only; a client asking for E.g.,
|
||
| selected operator bound with `x-llm-d-inference-ttl` using Go duration syntax. | ||
| * `noEndpointRequestTTL` — the queue-wait budget that replaces `defaultRequestTTL` while the pool has | ||
| no endpoints, where the queue acts as a scale-from-zero waiting room. Left unset it follows | ||
| `defaultRequestTTL`, so splitting the regimes is opt-in. Size it above pod startup (image pull plus | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -256,13 +256,18 @@ func (fc *FlowController) EnqueueAndWait( | |
| req.InferencePoolName(), | ||
| req.ModelName(), req.TargetModelName(), reqBytes) | ||
|
|
||
| // 1. Create the derived context that governs this request's lifecycle (Parent Cancellation + TTL). | ||
| reqCtx, cancel, enqueueTime, saturationTTL := fc.createRequestContext(ctx, req) | ||
| defer cancel() | ||
| // Capture the logical enqueue time before acquiring the flow. The band's TTL is only available | ||
| // from the acquired connection, but time spent acquiring it still counts against the queue budget. | ||
| enqueueTime := fc.clock.Now() | ||
|
|
||
| // 2. Acquire a lease for the Flow. | ||
| // We hold this lease for the entire duration of the request (Distribution + Queueing). | ||
| err := fc.withConnectionWithFallback(req, func(conn contracts.ActiveFlowConnection, effectiveReq flowcontrol.FlowControlRequest) error { | ||
| bandDefaultRequestTTL, bandDefaultRequestTTLSet := conn.DefaultRequestTTL() | ||
| reqCtx, cancel, saturationTTL := fc.createRequestContext( | ||
| ctx, effectiveReq, bandDefaultRequestTTL, bandDefaultRequestTTLSet, enqueueTime, | ||
| ) | ||
| defer cancel() | ||
|
|
||
| select { // Non-blocking check on controller lifecycle. | ||
| case <-fc.parentCtx.Done(): | ||
|
|
@@ -361,17 +366,8 @@ func (fc *FlowController) tryDistribution( | |
| saturationTTL time.Duration, | ||
| conn contracts.ActiveFlowConnection, | ||
| ) (*internal.FlowItem, error) { | ||
| // The item carries the saturation-regime budget: it is the request's own queue-wait budget, and ordering policies | ||
| // read it as such. A caller deadline that falls inside it clamps it, since the request cannot outlive its caller. | ||
| effectiveTTL := saturationTTL | ||
| if deadline, ok := reqCtx.Deadline(); ok { | ||
| if ttl := deadline.Sub(enqueueTime); ttl > 0 && (effectiveTTL <= 0 || ttl < effectiveTTL) { | ||
| effectiveTTL = ttl | ||
| } | ||
| } | ||
|
|
||
| // We must create a fresh FlowItem on each attempt as finalization is per-lifecycle. | ||
| item := internal.NewItem(req, effectiveTTL, enqueueTime, fc.logger) | ||
| item := internal.NewItem(req, saturationTTL, enqueueTime, fc.logger) | ||
|
|
||
| dp := conn.GetDataPlane() | ||
| _, err := dp.ManagedQueue(conn.FlowKey()) | ||
|
|
@@ -393,9 +389,9 @@ func (fc *FlowController) tryDistribution( | |
| // for handoff has not reached a queue, so it is not waiting on an endpoint to appear and the no-endpoint budget does | ||
| // not describe it; the regime-aware budget takes over once the processor owns the item. | ||
| distributeCtx := reqCtx | ||
| if effectiveTTL > 0 { | ||
| if saturationTTL > 0 { | ||
| var cancel context.CancelFunc | ||
| distributeCtx, cancel = context.WithDeadlineCause(reqCtx, enqueueTime.Add(effectiveTTL), types.ErrTTLExpired) | ||
| distributeCtx, cancel = context.WithDeadlineCause(reqCtx, enqueueTime.Add(saturationTTL), types.ErrTTLExpired) | ||
| defer cancel() | ||
| } | ||
|
|
||
|
|
@@ -467,21 +463,37 @@ func (fc *FlowController) awaitFinalization( | |
| func (fc *FlowController) createRequestContext( | ||
| ctx context.Context, | ||
| req flowcontrol.FlowControlRequest, | ||
| ) (context.Context, context.CancelFunc, time.Time, time.Duration) { | ||
| enqueueTime := fc.clock.Now() | ||
| saturationTTL := req.InitialEffectiveTTL() | ||
| if saturationTTL <= 0 { | ||
| saturationTTL = fc.config.DefaultRequestTTL | ||
| bandDefaultRequestTTL time.Duration, | ||
| bandDefaultRequestTTLSet bool, | ||
| enqueueTime time.Time, | ||
| ) (context.Context, context.CancelFunc, time.Duration) { | ||
| saturationTTL := fc.config.DefaultRequestTTL | ||
| if bandDefaultRequestTTLSet { | ||
| saturationTTL = bandDefaultRequestTTL | ||
| } | ||
| // A request may make the selected operator bound stricter, but not extend it. | ||
| if requestTTL := req.InitialEffectiveTTL(); requestTTL > 0 && (saturationTTL <= 0 || requestTTL < saturationTTL) { | ||
| saturationTTL = requestTTL | ||
| } | ||
|
|
||
| // A zero budget in either regime disables eviction there, so no backstop can be derived. | ||
| var reqCtx context.Context | ||
| var cancel context.CancelFunc | ||
| if saturationTTL > 0 && fc.config.NoEndpointRequestTTL > 0 { | ||
| backstop := max(saturationTTL, fc.config.NoEndpointRequestTTL) + 2*fc.config.ExpiryCleanupInterval | ||
| reqCtx, cancel := context.WithDeadlineCause(ctx, enqueueTime.Add(backstop), types.ErrTTLExpired) | ||
| return reqCtx, cancel, enqueueTime, saturationTTL | ||
| reqCtx, cancel = context.WithDeadlineCause(ctx, enqueueTime.Add(backstop), types.ErrTTLExpired) | ||
| } else { | ||
| reqCtx, cancel = context.WithCancel(ctx) | ||
| } | ||
|
|
||
| // Ordering policies use the saturation budget, clamped to a caller deadline that fires sooner. | ||
| if deadline, ok := reqCtx.Deadline(); ok { | ||
| if lifecycleTTL := deadline.Sub(enqueueTime); lifecycleTTL > 0 && | ||
| (saturationTTL <= 0 || lifecycleTTL < saturationTTL) { | ||
| saturationTTL = lifecycleTTL | ||
| } | ||
| } | ||
| reqCtx, cancel := context.WithCancel(ctx) | ||
| return reqCtx, cancel, enqueueTime, saturationTTL | ||
| return reqCtx, cancel, saturationTTL | ||
| } | ||
|
|
||
| // distributeRequest submits an item to the processor with graceful backpressure. | ||
|
|
@@ -502,6 +514,11 @@ func (fc *FlowController) distributeRequest( | |
| item *internal.FlowItem, | ||
| ) error { | ||
| reqID := item.OriginalRequest().ID() | ||
| select { | ||
| case <-ctx.Done(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit (non-blocking): The reason this early exit exists is not obvious from the code: |
||
| return fmt.Errorf("%w: request not accepted: %w", types.ErrRejected, ctx.Err()) | ||
| default: | ||
| } | ||
| if err := fc.processor.Submit(item); err == nil { | ||
| return nil | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reads as the whole queue-wait bound, but it only replaces
DefaultRequestTTL, which applies while the pool has endpoints.NoEndpointRequestTTLstays global and still governs the empty-pool regime for every band, and"0s"here does not make waiting unbounded while the pool is empty. Suggest mirroring the scope language the global field uses: