Skip to content

feat(function-autoscaler): add LLM gateway scaling - #727

Open
borao wants to merge 2 commits into
mainfrom
feat/function-autoscaler/llm-gateway-scaling
Open

feat(function-autoscaler): add LLM gateway scaling#727
borao wants to merge 2 commits into
mainfrom
feat/function-autoscaler/llm-gateway-scaling

Conversation

@borao

@borao borao commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Add LLM API gateway metrics as an autoscaling source between worker metrics and the existing control-plane fallback.

The autoscaler now prefers worker metrics, then LLM gateway metrics, and finally control-plane metrics. The selected source is cached for one hour.

Additional Details (optional for docs, build, test, refactor, ci, chore, style, and revert PRs)

  • Calculate LLM gateway utilization from the request-duration sum rate normalized by the function's current instance count.
  • Resolve function version IDs and NCA IDs from nvcf_function_info.
  • When multiple versions exist:
    • prefer a version that already has instances;
    • otherwise select an idle version;
    • cache the selection so subsequent scaling remains on that version.
  • Apply the function-level scaling delta only to the selected version.
  • Determine recent invocation activity from increases in the gateway request-duration metric, allowing the existing configurable scale-to-zero idle window to apply.
  • Extend function discovery to include functions visible through LLM gateway metrics.

For the Reviewer

Please focus on:

  • metric-source fallback and caching in work/mod.rs;
  • version selection and instance-delta handling;
  • LLM gateway discovery queries in work/discovery.rs;
  • preservation of the existing worker and control-plane pathways.

For QA (optional for docs, build, test, refactor, ci, chore, style, and revert PRs)

Validated with:

  • cargo fmt -p rs-autoscaler -- --check
  • cargo check -p rs-autoscaler --all-targets
  • cargo clippy -p rs-autoscaler --all-targets -- -D warnings
  • cargo test -p rs-autoscaler
    • 127 passed
    • 0 failed
    • 11 ignored because they require Cassandra or external services

Issues

NO-REF

Checklist

  • I am familiar with the Contributing Guidelines.
  • I have signed off my commits for Developer Certificate of Origin (DCO) compliance.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

Summary by CodeRabbit

  • New Features

    • Added support for LLM Gateway metrics as an autoscaling source.
    • Improved autoscaling to select gateway versions and routing targets.
    • Added gateway-aware invocation tracking and per-version scaling calculations.
    • Expanded metric discovery across gateway requests and deployment metadata.
  • Bug Fixes

    • Improved fallback behavior when preferred routing data is unavailable.
  • Tests

    • Expanded coverage for gateway discovery, sharding, routing, version selection, and scaling behavior.

Signed-off-by: Bora Oztekin <boztekin@nvidia.com>
@borao
borao requested a review from a team as a code owner August 8, 2026 01:01
@borao
borao requested a review from huaweic-nv August 8, 2026 01:01
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 43dde005-22b6-4409-88f2-922857bcc3f8

📥 Commits

Reviewing files that changed from the base of the PR and between 772185c and e683d42.

📒 Files selected for processing (2)
  • src/control-plane-services/function-autoscaler/crates/server/src/work/discovery.rs
  • src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs

📝 Walkthrough

Walkthrough

The autoscaler now discovers LLM Gateway metrics, selects metric sources through a shared routing cache, chooses gateway targets, and calculates source-specific scaling inputs and desired instance changes.

Changes

LLM Gateway autoscaling

Layer / File(s) Summary
Metric source and discovery
src/control-plane-services/function-autoscaler/crates/server/src/scaling/mod.rs, src/control-plane-services/function-autoscaler/crates/server/src/work/discovery.rs
MetricSource::LlmGateway is added. Sharded discovery now generates LLM Gateway queries and validates their shard and environment matchers.
Source-aware input gathering
src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs
Metric queries support three sources. Gateway metrics support version selection, target routing, aggregate instance counts, invocation detection, and cached routing decisions.
Autoscaling routing integration
src/control-plane-services/function-autoscaler/crates/server/src/server.rs, src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs
Server startup creates a shared MetricRoutingCache. Autoscaling tasks use gateway targets, selected NCA identifiers, and per-version desired-instance deltas.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: ⚪ Minimal · up to e683d

This change adds LLM gateway metrics as an autoscaling fallback and preserves the existing worker and control-plane paths; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant Autoscaler as run_autoscaling_logic_p0
  participant Gathering as scaling-input gathering
  participant Cache as MetricRoutingCache
  participant Gateway as gateway target selection
  Server->>Cache: create shared routing cache
  Server->>Autoscaler: pass routing cache
  Autoscaler->>Gathering: gather scaling inputs
  Gathering->>Cache: read or store routing decision
  Gathering->>Gateway: select gateway version and target
  Gateway-->>Gathering: return target and instance state
  Gathering-->>Autoscaler: return inputs and selected target
Loading

Suggested reviewers: huaweic-nv, kristinapathak

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and accurately describes the primary feature: LLM gateway scaling for the function autoscaler.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/function-autoscaler/llm-gateway-scaling

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

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🛡️ CodeQL Analysis

🚨 Found 2 issue(s)

Severity Breakdown:

  • 🔴 Errors: 0
  • 🟡 Warnings: 0
  • 🔵 Notes: 0
📋 Top Issues

🔗 View full details in Security tab

🕐 Last updated: 2026-08-08 01:04:34 UTC | Commit: 772185c

@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: 4

🧹 Nitpick comments (2)
src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs (2)

57-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a TTL to gateway_targets, and consider moving this cache into the scaling module.

Two points:

  1. sources expires after one hour. gateway_targets has only a capacity bound, so a version pin can live for the whole process lifetime. select_gateway_target does drop a stale pin when the pinned version disappears from nvcf_function_info or becomes idle while another version is active. A pin can still persist for days when all versions stay idle. A TTL makes the stickiness window explicit and bounded.
  2. The coding guidelines place policy caches and stickiness behavior in the scaling module. gateway_targets is a stickiness cache, and select_gateway_target is stickiness logic.
♻️ Proposed TTL change
 pub fn new_metric_routing_cache() -> MetricRoutingCache {
     let ttl = StdDuration::from_secs(60 * 60);
     MetricRoutingCache {
         sources: Cache::builder().time_to_live(ttl).build(),
-        gateway_targets: Cache::new(10_000),
+        gateway_targets: Cache::builder()
+            .max_capacity(10_000)
+            .time_to_live(ttl)
+            .build(),
     }
 }

As per coding guidelines: "Keep scaling logic, policy clients and caches, thresholds, and stickiness behavior within the scaling module."

🤖 Prompt for 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.

In `@src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs`
around lines 57 - 69, Update new_metric_routing_cache to configure
gateway_targets with the same one-hour time-to-live as sources while retaining
its existing capacity bound. Move MetricRoutingCache and the related
select_gateway_target stickiness logic into the scaling module, preserving their
current behavior and interfaces.

Source: Coding guidelines


419-438: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use the request counter for activity, and guard a zero-minute lookback.

Two points:

  1. This query measures activity with increase(..._duration_seconds_sum[...]). It reports idle when the gateway records a zero duration for every request in the window. llm_api_gateway_http_requests_total is already used by llm_gateway_metrics_present, and it is the direct activity signal. recently_invoked drives scale-to-zero, so a false negative terminates a live deployment.
  2. lookback_minutes comes from scale_to_zero_idle_timeout.as_secs() as i64 / 60 at Line 613. If the timeout is configured below 60 seconds, the value is 0 and the selector becomes [0m], which PromQL rejects. The error then propagates through ? and fails the whole gather for the function.
♻️ Proposed change
     let end_time = Utc::now();
+    let lookback_minutes = lookback_minutes.max(1);
     let query = format!(
-        r#"sum by(function_id) (increase(llm_api_gateway_http_request_duration_seconds_sum{{function_id="{}"}}[{}m])) > 0"#,
+        r#"sum by(function_id) (increase(llm_api_gateway_http_requests_total{{function_id="{}"}}[{}m])) > 0"#,
         function_id, lookback_minutes
     );
🤖 Prompt for 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.

In `@src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs`
around lines 419 - 438, Update llm_gateway_recently_invoked to query
llm_api_gateway_http_requests_total instead of the duration sum, preserving the
existing activity check for scale-to-zero decisions. Normalize a zero-minute
lookback to a valid positive PromQL range before constructing the query, so
sub-minute idle timeouts do not produce an invalid [0m] selector or propagate a
query error.
🤖 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
`@src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs`:
- Around line 343-351: The gateway scaling flow must prevent idle versions from
retaining surplus instances and must not reduce the selected version to zero
when shared capacity is redistributed. Update gateway_target_desired_instances
and the surrounding Line 812 per-version handling so non-selected versions
receive scale-down requests, while the selected target retains at least the
count required by the shared desired-total decision; revise the test covering
the current saturating-subtraction floor accordingly.
- Around line 353-417: Scope all gateway metrics to the current environment:
update get_gateway_target in
src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs
(lines 353-417) to accept env and ignore_env and apply the appropriate matcher
to both nvcf_function_instances_current and nvcf_function_info; align the
LlmGateway numerator and denominator in the same file (lines 115-141), or
document at both sites if either metric lacks an environment label. Update
discovery in
src/control-plane-services/function-autoscaler/crates/server/src/work/discovery.rs
(lines 188-199) to reuse get_timeseries_db_query’s aws_env matcher while
honoring ignore_env, and adjust the assertion at line 995 accordingly.
- Around line 115-141: Update the MetricSource::LlmGateway query in the metric
query construction to divide the in-flight request rate by both current
instances and nvcf_function_concurrency, matching the ControlPlane utilization
formula. Preserve the existing function_id grouping and zero-safe denominator
behavior so the result remains a percentage calibrated for decide_scaling.
- Around line 502-637: The TimeseriesDb helper errors are not currently recorded
in the request span. Update the existing tracing instrumentation on the shared
request execution function reached via query_range to include the returned error
field using err, preserving the existing helper behavior and span coverage.

---

Nitpick comments:
In
`@src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs`:
- Around line 57-69: Update new_metric_routing_cache to configure
gateway_targets with the same one-hour time-to-live as sources while retaining
its existing capacity bound. Move MetricRoutingCache and the related
select_gateway_target stickiness logic into the scaling module, preserving their
current behavior and interfaces.
- Around line 419-438: Update llm_gateway_recently_invoked to query
llm_api_gateway_http_requests_total instead of the duration sum, preserving the
existing activity check for scale-to-zero decisions. Normalize a zero-minute
lookback to a valid positive PromQL range before constructing the query, so
sub-minute idle timeouts do not produce an invalid [0m] selector or propagate a
query error.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 790bb3fa-c58a-4336-af88-301c8ccb43b8

📥 Commits

Reviewing files that changed from the base of the PR and between 9dbf12f and 772185c.

📒 Files selected for processing (4)
  • src/control-plane-services/function-autoscaler/crates/server/src/scaling/mod.rs
  • src/control-plane-services/function-autoscaler/crates/server/src/server.rs
  • src/control-plane-services/function-autoscaler/crates/server/src/work/discovery.rs
  • src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs

Comment on lines +343 to +351
fn gateway_target_desired_instances(
desired_total: usize,
total_current: usize,
target_current: usize,
) -> usize {
target_current
.saturating_add(desired_total)
.saturating_sub(total_current)
}

@coderabbitai coderabbitai Bot Aug 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

The gateway delta model cannot scale down idle versions, and it can zero the selected version.

gateway_target_desired_instances converts a total desired count into a per-version count with target_current + desired_total - total_current, floored at 0 by saturating_sub. Two failure modes follow, because Line 812 returns early for every version that is not the selected target:

  • Non-selected versions never receive a scaling request. Their instances stay allocated indefinitely. total_current therefore stays high, and the delta stays small.
  • When total_current - target_current >= desired_total, the target gets 0. Example: total_current = 10 split as target 4 and an idle version 6, with desired_total = 5. The result is 0 for the target, while the idle version keeps 6. The active version drops to zero and the surplus stays.

The test at Line 1147 pins this floor behavior, so the intent is unclear. Either issue scale-down requests for non-selected versions, or clamp the target to at least the count that the shared decision requires. Do you want me to open an issue to track the multi-version scale-down path?

🤖 Prompt for 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.

In `@src/control-plane-services/function-autoscaler/crates/server/src/work/mod.rs`
around lines 343 - 351, The gateway scaling flow must prevent idle versions from
retaining surplus instances and must not reduce the selected version to zero
when shared capacity is redistributed. Update gateway_target_desired_instances
and the surrounding Line 812 per-version handling so non-selected versions
receive scale-down requests, while the selected target retains at least the
count required by the shared desired-total decision; revise the test covering
the current saturating-subtraction floor accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentional. We apply the function-level delta to one version at a time; the next cycle reselects the active version and converges without concurrent writes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@borao, the current implementation does not reselect another active version on the next cycle.

MetricRoutingCache.gateway_targets pins the selected version. select_gateway_target keeps that pinned version while it has active instances. The per-version early return then prevents all other versions from receiving a scale request.

In the total_current = 10, target = 4, and desired total = 5 case, the target becomes 0. The idle version remains at 6. Subsequent cycles do not converge because the target remains pinned until it reaches zero, and no request reduces the idle version.

The finding remains valid for the current target-selection and cache behavior.


🧠 Learnings used
Learnt from: CR
Repo: NVIDIA/nvcf PR: 0
File: src/control-plane-services/function-autoscaler/AGENTS.md:0-0
Timestamp: 2026-07-21T18:21:56.834Z
Learning: Applies to src/control-plane-services/function-autoscaler/crates/server/src/scaling/**/*.rs : Keep scaling logic, policy clients and caches, thresholds, and stickiness behavior within the scaling module.

Learnt from: CR
Repo: NVIDIA/nvcf PR: 0
File: src/control-plane-services/function-autoscaler/AGENTS.md:0-0
Timestamp: 2026-07-21T18:21:56.834Z
Learning: Applies to src/control-plane-services/function-autoscaler/**/*.rs : Implement the service as a scaling loop that queries utilization metrics, applies per-function static or custom gRPC scaling policies, and computes the desired instance count.

You are interacting with an AI system.

@borao

borao commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Regarding the routing-cache TTL/module-placement nit: leaving this unchanged. Pins are capacity-bounded and revalidated on every lookup; retaining idle pins provides stable version selection. Moving the code can be a separate refactor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant