Skip to content

feat(llm-client): add vllm hidden-state probe - #185

Open
anniesurla wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
anniesurla:asurla/feat-prefill-probe-vllm-client
Open

feat(llm-client): add vllm hidden-state probe#185
anniesurla wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
anniesurla:asurla/feat-prefill-probe-vllm-client

Conversation

@anniesurla

@anniesurla anniesurla commented Jul 29, 2026

Copy link
Copy Markdown

What

  • Adds a Rust VllmHiddenStateProbe for vLLM ExampleHiddenStatesConnector artifacts.
  • Validates and token-mean pools safetensors hidden states, with bounded lock waiting, cleanup, and stale-artifact reaping.
  • Exposes typed configuration, feature, and error APIs from switchyard-llm-client.

Why

Provides the transport implementation consumed by the transport-independent prefill classifier in #174.

Depends on #174. Until #174 merges, GitHub will also show its commit in this stacked PR.

How tested

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace
  • uv run ruff check .
  • uv run mypy switchyard
  • uv run pytest tests/ -v -m "not integration" — 1718 passed, 12 skipped, 28 deselected
  • No live provider calls

Checklist

  • Unit tests cover the new client, tensor validation, cleanup, and failure paths.
  • Commit is DCO signed off.

Notes for reviewers

The intended delta is the single feat(llm-client) commit on top of #174. Token-aware pricing remains a separate follow-up.

Summary by CodeRabbit

  • New Features
    • Added hidden-state extraction from compatible language model endpoints.
    • Added prompt-feature routing that selects between strong and weak completion targets.
    • Added cost-aware routing based on predicted quality and configured costs.
    • Added artifact validation, feature pooling, caching, and safe fallback behavior.
  • Bug Fixes
    • Added validation and cleanup safeguards for generated feature artifacts.
  • Tests
    • Added comprehensive coverage for extraction, routing, caching, validation, and fallback scenarios.

asurla1998 added 2 commits July 29, 2026 17:19
Signed-off-by: asurla1998 <asurla1998@users.noreply.huggingface.co>
Signed-off-by: asurla1998 <asurla1998@users.noreply.huggingface.co>
@anniesurla
anniesurla requested a review from a team as a code owner July 29, 2026 18:57
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a vLLM hidden-state extraction probe and a learned prefill classifier. The classifier validates and runs router artifacts, applies cost-aware weak/strong selection, caches successful decisions, and integrates with existing classifier routing.

Changes

Prefill routing pipeline

Layer / File(s) Summary
vLLM probe contract
crates/libsy-llm-client/Cargo.toml, crates/libsy-llm-client/src/lib.rs, crates/libsy-llm-client/src/vllm_hidden_state_probe.rs
Adds the public probe API, configuration, typed errors, HTTP request handling, and hidden-state response contract.
Probe artifact synchronization and pooling
crates/libsy-llm-client/src/vllm_hidden_state_probe.rs
Validates artifact paths, coordinates lock files, reaps stale artifacts, parses safetensors data, and computes pooled features with cleanup.
Router artifact validation and inference
crates/libsy/Cargo.toml, crates/libsy/src/algorithms/prefill_probe/artifact.rs
Loads and validates router metadata and tensors, then performs standardization, PCA projection, dense inference, and ensemble probability calculation.
Cost-aware tier policy
crates/libsy/src/algorithms/prefill_probe/policy.rs
Adds validated weak/strong tier selection using correctness probabilities and normalized costs.
Prefill classifier integration and caching
crates/libsy/src/algorithms.rs, crates/libsy/src/algorithms/prefill_probe.rs, crates/libsy/src/lib.rs
Adds the probe trait and classifier, task extraction, artifact-backed routing, LRU caching, strong-target fallback, public exports, and crate documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Poem

I’m a bunny with features, hopping through the stack,
Pooling hidden states and sending results back.
Weak or strong, the cache knows the way,
Safetensors bloom at the end of the day.
Hop-hop—new routes are ready to play!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches a major part of the PR: the new vLLM hidden-state probe, though it omits the prefill-classifier additions.
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.

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

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

🧹 Nitpick comments (9)
crates/libsy-llm-client/src/vllm_hidden_state_probe.rs (3)

149-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

HTTP client build failure should be a Configuration error.

map_reqwest_error classifies a builder failure as Transport (or Timeout), but nothing has been sent yet — this is purely a bad-config outcome, and Configuration exists for exactly that. Callers matching on Transport may wrongly retry.

♻️ Suggested change
         let client = reqwest::Client::builder()
             .timeout(config.request_timeout)
             .build()
-            .map_err(map_reqwest_error)?;
+            .map_err(|error| {
+                configuration_error(format!("failed to build probe HTTP client: {error}"))
+            })?;
🤖 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 `@crates/libsy-llm-client/src/vllm_hidden_state_probe.rs` around lines 149 -
152, Update the reqwest client construction in the probe initialization flow to
map `.build()` failures directly to the existing `Configuration` error variant
instead of using `map_reqwest_error`. Keep `map_reqwest_error` for runtime
request failures and preserve the existing successful client construction path.

168-169: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Every probe pays a full directory scan before its request.

reap_stale_artifacts walks the entire hidden-state directory (and stats + lock-probes each .safetensors entry) on the blocking pool ahead of each extract. On a busy shared connector directory this is O(files) blocking I/O per routing decision, directly on the prefill-latency path. Consider amortizing it — a periodic background sweep, or gating on an elapsed-since-last-sweep timestamp — rather than running it inline per request.

🤖 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 `@crates/libsy-llm-client/src/vllm_hidden_state_probe.rs` around lines 168 -
169, The extract method currently performs the full reap_stale_artifacts
directory scan inline for every request. Amortize stale-artifact cleanup by
gating reap_stale_artifacts with an elapsed-since-last-sweep check or moving it
to a periodic background task, while preserving cleanup behavior and avoiding
blocking I/O on the extract request path.

21-23: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider exposing the artifact wait timeout through VllmHiddenStateProbeConfig.

artifact_wait_timeout and stale_artifact_retention are struct fields but are only ever set from the consts, so they are effectively unconfigurable. A hard 1s ceiling on waiting for vLLM to release the synchronization lock is the most likely first failure mode under prefill load, and request_timeout is already caller-tunable — the wait bound deserves the same treatment (optional fields defaulting to these consts).

Also applies to: 118-119

🤖 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 `@crates/libsy-llm-client/src/vllm_hidden_state_probe.rs` around lines 21 - 23,
Expose artifact_wait_timeout and stale_artifact_retention as optional fields on
VllmHiddenStateProbeConfig, defaulting to ARTIFACT_WAIT_TIMEOUT and
STALE_ARTIFACT_RETENTION when unset. Update configuration construction and the
probe’s wait/retention logic to use the resolved config values, while preserving
the existing constants as defaults.
crates/libsy-llm-client/Cargo.toml (1)

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

Consider workspace-backing safetensors (and tempfile).

Every other dependency added here is *.workspace = true, and crates/libsy/src/algorithms/prefill_probe/artifact.rs in this same stack also depends on safetensors. Two independent pins can drift and pull in duplicate versions of the tensor parser.

♻️ Suggested change
-safetensors = "0.4"
+safetensors.workspace = true

Add to the workspace manifest [workspace.dependencies]:

safetensors = "0.4"

Also applies to: 29-29

🤖 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 `@crates/libsy-llm-client/Cargo.toml` at line 21, Move the safetensors
dependency to the workspace manifest’s [workspace.dependencies] with version
0.4, then update the libsy-llm-client dependency declaration to use workspace
backing; apply the same workspace-backed dependency change to tempfile where it
is declared.
crates/libsy/src/algorithms/prefill_probe/artifact.rs (2)

588-604: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider accumulating dense products in f64 for parity with project_pca.

project_pca (Line 547) accumulates in f64 while dense_layer sums 200/256/128-wide dot products in f32, so trunk logits can drift from the exported reference math for large activations. Cheap to align.

♻️ Proposed change
             let value = row
                 .iter()
                 .zip(input)
-                .map(|(weight, input)| *weight * *input)
-                .sum::<f32>()
-                + *bias;
+                .map(|(weight, input)| f64::from(*weight) * f64::from(*input))
+                .sum::<f64>() as f32
+                + *bias;
🤖 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 `@crates/libsy/src/algorithms/prefill_probe/artifact.rs` around lines 588 -
604, Update dense_layer’s dot-product accumulation to use f64, matching
project_pca’s reference-math precision, then convert the summed result back to
f32 before adding the bias and applying the existing finite-value and ReLU
handling.

181-227: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test helper can produce non-finite logits for degenerate probabilities.

(probability / (1.0 - probability)).ln() yields inf at 1.0 and -inf/NaN at 0.0, which would then fail the finite checks in ensemble_probabilities. Current callers pass interior values, but a debug assertion or clamp keeps the helper safe for future tests.

🤖 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 `@crates/libsy/src/algorithms/prefill_probe/artifact.rs` around lines 181 -
227, Update with_test_probabilities so boundary probabilities cannot produce
non-finite logits before being stored in ensemble output biases. Clamp or
otherwise validate each probability to a finite interior range before
calculating the logarithm, while preserving existing behavior for valid interior
probabilities.
crates/libsy/src/algorithms/prefill_probe.rs (2)

300-322: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Emptiness check trims but the distinctness check does not.

"weak" and " weak" pass strong_target != weak_target yet resolve to the same semantic target downstream (and routing_tier compares raw strings). Compare trimmed values, or store trimmed targets.

♻️ Proposed change
-    if config.strong_target == config.weak_target {
+    if config.strong_target.trim() == config.weak_target.trim() {
         return Err(config_error(
             "strong_target and weak_target must be distinct",
         ));
     }
🤖 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 `@crates/libsy/src/algorithms/prefill_probe.rs` around lines 300 - 322, Update
validate_config to compare trimmed strong_target and weak_target values for
distinctness, and ensure downstream routing_tier uses the same normalized target
values or equivalent trimmed comparisons. Preserve the existing non-empty
validation and error behavior while preventing whitespace-only differences from
being treated as distinct targets.

341-360: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Terminus task text is used verbatim without trimming.

terminus_task_instruction returns the raw slice between headers, so leading/trailing whitespace variations of the same task produce different cache keys and different probe inputs. Trimming (and rejecting empty after trim) would make keys stable.

♻️ Proposed change
 fn terminus_task_instruction(instruction: &str) -> Option<&str> {
     let (_, task_and_terminal) = instruction.split_once(TERMINUS_TASK_DESCRIPTION_HEADER)?;
     let (task, _) = task_and_terminal.split_once(TERMINUS_TERMINAL_STATE_HEADER)?;
-    (!task.is_empty()).then_some(task)
+    let task = task.trim();
+    (!task.is_empty()).then_some(task)
 }
🤖 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 `@crates/libsy/src/algorithms/prefill_probe.rs` around lines 341 - 360, Trim
the task slice returned by terminus_task_instruction before validating and
returning it, so leading or trailing whitespace does not affect probe inputs or
cache keys. Ensure whitespace-only tasks are rejected after trimming, while
preserving the existing header parsing and fallback behavior in probe_input.
crates/libsy/Cargo.toml (1)

19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update lru and safetensors pins

lru = "0.12" and safetensors = "0.4" are behind current releases; refresh them unless you need to stay on these lines.

🤖 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 `@crates/libsy/Cargo.toml` around lines 19 - 25, Update the dependency pins for
lru and safetensors in the crate manifest to current compatible releases,
replacing the existing 0.12 and 0.4 constraints. Keep the remaining dependency
configuration unchanged unless compatibility requires retaining the current
major lines.
🤖 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 `@crates/libsy-llm-client/src/vllm_hidden_state_probe.rs`:
- Around line 466-473: Update cleanup_artifact_files so removing the main
artifact treats a filesystem NotFound error as successful cleanup, matching the
existing lock-file handling for lock_path. Continue converting other removal
errors through artifact_error, while preserving the current cleanup flow.
- Around line 484-498: Update cleanup_stale_artifacts in
crates/libsy-llm-client/src/vllm_hidden_state_probe.rs at lines 484-498 to skip
entries when entry, file_type, metadata, or modified lookups fail due to
concurrent disappearance (at minimum ErrorKind::NotFound), continuing the sweep
instead of returning an error; also update remove_file handling at lines 466-473
to treat ErrorKind::NotFound as success, consistent with the lock-file handling.

---

Nitpick comments:
In `@crates/libsy-llm-client/Cargo.toml`:
- Line 21: Move the safetensors dependency to the workspace manifest’s
[workspace.dependencies] with version 0.4, then update the libsy-llm-client
dependency declaration to use workspace backing; apply the same workspace-backed
dependency change to tempfile where it is declared.

In `@crates/libsy-llm-client/src/vllm_hidden_state_probe.rs`:
- Around line 149-152: Update the reqwest client construction in the probe
initialization flow to map `.build()` failures directly to the existing
`Configuration` error variant instead of using `map_reqwest_error`. Keep
`map_reqwest_error` for runtime request failures and preserve the existing
successful client construction path.
- Around line 168-169: The extract method currently performs the full
reap_stale_artifacts directory scan inline for every request. Amortize
stale-artifact cleanup by gating reap_stale_artifacts with an
elapsed-since-last-sweep check or moving it to a periodic background task, while
preserving cleanup behavior and avoiding blocking I/O on the extract request
path.
- Around line 21-23: Expose artifact_wait_timeout and stale_artifact_retention
as optional fields on VllmHiddenStateProbeConfig, defaulting to
ARTIFACT_WAIT_TIMEOUT and STALE_ARTIFACT_RETENTION when unset. Update
configuration construction and the probe’s wait/retention logic to use the
resolved config values, while preserving the existing constants as defaults.

In `@crates/libsy/Cargo.toml`:
- Around line 19-25: Update the dependency pins for lru and safetensors in the
crate manifest to current compatible releases, replacing the existing 0.12 and
0.4 constraints. Keep the remaining dependency configuration unchanged unless
compatibility requires retaining the current major lines.

In `@crates/libsy/src/algorithms/prefill_probe.rs`:
- Around line 300-322: Update validate_config to compare trimmed strong_target
and weak_target values for distinctness, and ensure downstream routing_tier uses
the same normalized target values or equivalent trimmed comparisons. Preserve
the existing non-empty validation and error behavior while preventing
whitespace-only differences from being treated as distinct targets.
- Around line 341-360: Trim the task slice returned by terminus_task_instruction
before validating and returning it, so leading or trailing whitespace does not
affect probe inputs or cache keys. Ensure whitespace-only tasks are rejected
after trimming, while preserving the existing header parsing and fallback
behavior in probe_input.

In `@crates/libsy/src/algorithms/prefill_probe/artifact.rs`:
- Around line 588-604: Update dense_layer’s dot-product accumulation to use f64,
matching project_pca’s reference-math precision, then convert the summed result
back to f32 before adding the bias and applying the existing finite-value and
ReLU handling.
- Around line 181-227: Update with_test_probabilities so boundary probabilities
cannot produce non-finite logits before being stored in ensemble output biases.
Clamp or otherwise validate each probability to a finite interior range before
calculating the logarithm, while preserving existing behavior for valid interior
probabilities.
🪄 Autofix (Beta)

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: 93deeaed-1b3e-4ce8-904c-c696d85cb308

📥 Commits

Reviewing files that changed from the base of the PR and between 1f5d173 and 048122c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (9)
  • crates/libsy-llm-client/Cargo.toml
  • crates/libsy-llm-client/src/lib.rs
  • crates/libsy-llm-client/src/vllm_hidden_state_probe.rs
  • crates/libsy/Cargo.toml
  • crates/libsy/src/algorithms.rs
  • crates/libsy/src/algorithms/prefill_probe.rs
  • crates/libsy/src/algorithms/prefill_probe/artifact.rs
  • crates/libsy/src/algorithms/prefill_probe/policy.rs
  • crates/libsy/src/lib.rs

Comment on lines +466 to +473
fn cleanup_artifact_files(path: &Path) -> VllmHiddenStateProbeResult<()> {
let lock_path = companion_lock_path(path);
std::fs::remove_file(path).map_err(|error| {
artifact_error(format!(
"hidden-state artifact cleanup error for {}: {error}",
path.display()
))
})?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Treat a already-removed artifact as successful cleanup.

The lock file's NotFound is tolerated (Line 476) but the artifact's is not, so a concurrent reaper or a second probe instance removing the file first converts a fully successful extraction into an Artifact error at Line 458. Mirror the lock-file handling. (Shares a root cause with the sweep issue above — see the consolidated note.)

🤖 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 `@crates/libsy-llm-client/src/vllm_hidden_state_probe.rs` around lines 466 -
473, Update cleanup_artifact_files so removing the main artifact treats a
filesystem NotFound error as successful cleanup, matching the existing lock-file
handling for lock_path. Continue converting other removal errors through
artifact_error, while preserving the current cleanup flow.

Comment on lines +484 to +498
fn cleanup_stale_artifacts(root: &Path, retention: Duration) -> VllmHiddenStateProbeResult<()> {
let entries = std::fs::read_dir(root).map_err(|error| {
artifact_error(format!(
"failed to scan hidden-state directory {}: {error}",
root.display()
))
})?;
let now = SystemTime::now();
for entry in entries {
let entry = entry.map_err(|error| {
artifact_error(format!(
"failed to inspect hidden-state directory {}: {error}",
root.display()
))
})?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Filesystem races in the shared hidden-state directory are treated as hard failures. Both sites assume this probe is the only mutator of hidden_states_dir, but vLLM writes there and concurrent probe instances reap there, so a file disappearing mid-operation is expected rather than exceptional. Classify concurrent disappearance as benign in both places.

  • crates/libsy-llm-client/src/vllm_hidden_state_probe.rs#L484-L498: skip entries whose entry/file_type/metadata/modified lookups fail (at minimum NotFound) and continue the sweep, instead of returning Err and failing extract before the HTTP request is sent.
  • crates/libsy-llm-client/src/vllm_hidden_state_probe.rs#L466-L473: treat ErrorKind::NotFound on remove_file(path) as success, matching the lock-file handling immediately below it.
📍 Affects 1 file
  • crates/libsy-llm-client/src/vllm_hidden_state_probe.rs#L484-L498 (this comment)
  • crates/libsy-llm-client/src/vllm_hidden_state_probe.rs#L466-L473
🤖 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 `@crates/libsy-llm-client/src/vllm_hidden_state_probe.rs` around lines 484 -
498, Update cleanup_stale_artifacts in
crates/libsy-llm-client/src/vllm_hidden_state_probe.rs at lines 484-498 to skip
entries when entry, file_type, metadata, or modified lookups fail due to
concurrent disappearance (at minimum ErrorKind::NotFound), continuing the sweep
instead of returning an error; also update remove_file handling at lines 466-473
to treat ErrorKind::NotFound as success, consistent with the lock-file handling.

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