Skip to content

feat(operator): add SGLang selection topology controller#11036

Open
yankay wants to merge 6 commits into
ai-dynamo:mainfrom
yankay:feat/sglang-selection-adapter
Open

feat(operator): add SGLang selection topology controller#11036
yankay wants to merge 6 commits into
ai-dynamo:mainfrom
yankay:feat/sglang-selection-adapter

Conversation

@yankay

@yankay yankay commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

This adds a Kubernetes selection topology controller and external-sglang adapter for the runtime-free standalone selection service. The operator discovers annotated external SGLang worker Services, probes /v1/models, /model_info, and /server_info, normalizes the result into WorkerRequest, and reconciles the selection service /workers catalog.

This builds on the HTTP selection service from #10641.

Scope is intentionally narrow: this is a control-plane catalog path for aggregated/raw SGLang workers. It does not add Gateway/EPP forwarding, reservation lifecycle handling, P/D orchestration, multinode SGLang aggregation, or other engine adapters.

Details

  • Add the external-sglang adapter and selection topology controller.
  • Register workers at the worker Service ownership boundary and fan out the desired catalog to every ready selector replica behind a selector Kubernetes Service.
  • Requeue worker Services when selector Services or selector EndpointSlices change, and periodically replay successful reconciles to bound catalog recovery.
  • Fail closed on invalid required SGLang metadata, including invalid KV-event metadata and invalid capacity fields.
  • Best-effort deactivate stale/orphan operator-owned records while preserving the selection service's tombstone-based DELETE /workers/{worker_id} semantics.
  • Reject overlapping annotated worker Services that target the same ready Pod endpoint to avoid Pod-derived worker_id collisions.
  • Add real SGLang metadata testdata and runtime-free external SGLang example manifests.

Known gaps:

  • Covers single-routable, aggregated external SGLang workers. Multi-node SGLang groups, real dp_size > 1 SGLang deployments, follower KV-event aggregation, and restart-stable logical worker IDs are follow-ups.
  • Use selector Kubernetes Service DNS for operator-managed fan-out and Pod replacement replay. Raw IPs, localhost, host URLs, and external load balancers are treated as single fixed HTTP targets.
  • nvidia.com/dynamo-selection-require-kv-events controls operator-side metadata validation only. It does not override selection-service schedulability.
  • The operator validates KV-event metadata shape, not selector-to-worker TCP reachability.
  • Stale/orphan deactivation is best effort. The selection service currently keeps deactivated workers as visible unschedulable tombstones; purge, TTL/GC, and active-only catalog views are follow-ups.

Related Issues

Validation

  • Operator Go unit tests, Rust selection service tests, and git diff --check passed.
  • Manual Kind e2e smoke validated catalog registration, /workers, /ready, /select, KV event ingestion, selector Service fan-out/replay, stale/orphan deactivation, and fail-closed metadata handling.

@yankay yankay requested review from a team as code owners June 29, 2026 02:21
@copy-pr-bot

copy-pr-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@yankay yankay temporarily deployed to external_collaborator June 29, 2026 02:21 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi yankay! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added feat external-contribution Pull request is from an external contributor documentation Improvements or additions to documentation deployment::k8s Relates to dynamo deployment in kubernetes labels Jun 29, 2026
@datadog-official

datadog-official Bot commented Jun 29, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 1 Pipeline job failed

Docs link check | lychee   View in Datadog   GitHub Actions

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: ba2e48a | Docs | Give us feedback!

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93cc0d415b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +843 to +845
targetURL := parsedSelectorURL.parsed
targetURL.Host = net.JoinHostPort(endpoint.Addresses[0], strconv.Itoa(int(targetPort)))
selectorTargetURLSet.Insert(targetURL.String())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the Service hostname for HTTPS fan-out

When the selector URL is an https:// Kubernetes Service DNS name, this rewrites the request URL to the Pod IP, so Go sends SNI and verifies the certificate against that IP rather than the original Service hostname. In clusters that put TLS on the selection service with a normal selector.namespace.svc certificate, every fan-out request fails and workers never register; either avoid endpoint-IP fan-out for HTTPS or preserve the original server name/Host when dialing replicas.

Useful? React with 👍 / 👎.

Comment thread deploy/operator/internal/controller/selection_topology_controller.go Outdated
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a SelectionTopologyReconciler Kubernetes controller that watches annotated worker Service objects and reconciles them into a runtime-free selection service catalog. The change introduces a Go selectionservice HTTP client, an SGLang metadata HTTP client with KV events endpoint resolution and worker request builder, supporting types, test suites, and a Kustomize example deploying SGLang + selection service. On the Rust side, a metadata string map is added to WorkerRequest/WorkerRecord and patchable via WorkerPatchRequest.

Changes

SelectionTopology Controller + SGLang Worker Registration

Layer / File(s) Summary
Rust selection service metadata field
lib/kv-router/src/services/selection/types.rs, lib/kv-router/src/services/selection/core/mod.rs, lib/kv-router/src/services/selection/tests.rs, docs/components/router/standalone-selection.md
Adds metadata: HashMap<String, String> to WorkerRequest, WorkerPatchRequest, and WorkerCatalogRecord in Rust; propagates through new/apply_patch; updates test helpers and adds a metadata round-trip test; documents the field and updated DELETE deactivation semantics.
Go selection service types and HTTP client
deploy/operator/internal/selectionservice/types.go, deploy/operator/internal/selectionservice/client.go, deploy/operator/internal/selectionservice/client_test.go
Defines WorkerRequest, WorkerRecord, WorkerLifecycle, and normalization helpers; implements Client with UpsertWorker, ListWorkers, and DeleteWorker; tests upsert/delete round-trips, URL construction, error wrapping, and timeout handling.
SGLang metadata types and HTTP client
deploy/operator/internal/enginemetadata/sglang/types.go, deploy/operator/internal/enginemetadata/sglang/client.go, deploy/operator/internal/enginemetadata/sglang/client_test.go, deploy/operator/internal/enginemetadata/sglang/testdata/*
Defines MetadataSnapshot and supporting structs; implements MetadataClient.Fetch hitting /v1/models, /model_info, /server_info; adds URL normalization; tests fetch, URL building, HTTP/JSON error wrapping, and timeouts; adds fixture JSON files.
SGLang KV events resolution and worker request builder
deploy/operator/internal/enginemetadata/sglang/kv_events.go, deploy/operator/internal/enginemetadata/sglang/worker.go, deploy/operator/internal/enginemetadata/sglang/worker_test.go
Implements kvEventsEndpoints for descriptor- and config-based ZMQ endpoint derivation; implements BuildWorkerRequest converting MetadataSnapshot into a selectionservice.WorkerRequest including model ID resolution, generation/mode validation, Eagle classification, block size, DP sizing, KV event endpoints, and topology domains; tests all behaviors including fail-closed and fixture-backed cases.
New constants and SGLang adapter wiring
deploy/operator/internal/consts/consts.go, deploy/operator/internal/controller/selection_topology_adapter_sglang.go
Adds Kubernetes annotation constants (dynamo-selection-*) and ResourceTypeSelectionTopology; implements externalSGLangSelectionAdapter with BuildWorker delegating to the SGLang client and worker builder.
SelectionTopologyReconciler: core reconcile loop and catalog plans
deploy/operator/internal/controller/selection_topology_controller.go (lines 1–224, 226–301, 1231–1307)
Implements Reconcile (load/validate worker Service, compute worker and selector targets, build catalog plans, reconcile workers, deactivate stale workers); buildSelectorCatalogPlans with fail-closed and requeue logic; and the mutex-guarded in-memory known-worker cache.
Controller watch setup, mapping helpers, and worker discovery
deploy/operator/internal/controller/selection_topology_controller.go (lines 303–1229)
Implements SetupWithManager with field indexers and watch handlers; Pod/EndpointSlice/Service mapping and predicate helpers; worker target extraction; selector URL resolution and scoping; EndpointSlice port selection; catalog record comparison; orphan/stale deactivation; and utility helpers.
SelectionTopologyReconciler tests
deploy/operator/internal/controller/selection_topology_controller_test.go
Adds a comprehensive test suite with fake SGLang and selection servers covering initial reconciliation, KV events required/optional handling, fail-closed deactivation, orphan/stale cleanup, selector URL changes, drift reposting, watch mapper and predicate correctness.
Operator controller registration
deploy/operator/cmd/main.go
Registers SelectionTopologyReconciler with the controller-runtime manager in registerControllers.
Documentation and example manifests
docs/kubernetes/dynamo-operator.md, examples/README.md, examples/runtime-free-router/...
Adds SelectionTopologyController to operator architecture docs with annotated Service YAML and contracts; adds runtime-free-router example README and SGLang Kustomize manifests deploying selection service and annotated SGLang worker.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.57% 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
Title check ✅ Passed The title is concise and accurately reflects the main change: adding the SGLang selection topology controller.
Description check ✅ Passed The description covers overview, details, reviewer start points, related issue, gaps, and validation; it is mostly complete.
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.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@deploy/operator/internal/enginemetadata/sglang/kv_events.go`:
- Around line 18-22: The endpoint selection in kvEventsEndpoints currently
prefers the legacy KVEvents descriptor when both KVEvents and KVEventsConfig are
present, which can reject valid mixed payloads. Update kvEventsEndpoints to
treat KVEventsConfig as the प्राथमिक source and only fall back to
kvEventsDescriptorEndpoints when KVEventsConfig is absent or empty, keeping the
existing kvEventsConfigEndpoints path and preserving BuildWorkerRequest
behavior.

In `@deploy/operator/internal/enginemetadata/sglang/worker_test.go`:
- Around line 153-172: The test currently only exercises the legacy kv_events
path, so it does not validate the structured kv_events_config branch in
BuildWorkerRequest. Update
TestBuildWorkerRequestReadsRealStructuredKVEventsMetadata to remove or nil out
metadata.ServerInfo.KVEvents, then assert the structured payload is decoded via
KVEventsConfig instead of KVEventsEndpoints; alternatively, change
realStructuredKVEventsFixtureSnapshot to provide an object-shaped
kv_events_config fixture so the test explicitly covers structured metadata
handling.

In `@docs/components/router/standalone-selection.md`:
- Around line 84-90: Update the standalone-selection documentation to state that
PATCHing metadata replaces the entire map rather than merging entries. In the
section describing POST /workers, PATCH /workers/{worker_id}, and GET /workers
behavior, clarify that the selection service’s metadata handling (as implemented
by selection/types.rs in the metadata setter) overwrites the existing map, so
clients must resend all desired keys when updating worker metadata.

In `@examples/runtime-free-router/sglang/kustomization.yaml`:
- Around line 11-14: The kustomization for the selector overlay still points to
the plain dynamo-frontend image, so the deployed Pod may not contain
dynamo.select_service. Update the image reference in the overlay to a published
selector-capable image that includes the select-service feature, or make the
image overridable/required before deployment. Use the existing kustomization
fields newName and newTag to locate the image settings and ensure the referenced
image is compatible with running python -m dynamo.select_service.
🪄 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: ec5f4f4d-8d8c-4a5c-8d49-41c87206f8ff

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb539f and 93cc0d4.

📒 Files selected for processing (30)
  • deploy/operator/cmd/main.go
  • deploy/operator/internal/consts/consts.go
  • deploy/operator/internal/controller/selection_topology_adapter_sglang.go
  • deploy/operator/internal/controller/selection_topology_controller.go
  • deploy/operator/internal/controller/selection_topology_controller_test.go
  • deploy/operator/internal/enginemetadata/sglang/client.go
  • deploy/operator/internal/enginemetadata/sglang/client_test.go
  • deploy/operator/internal/enginemetadata/sglang/kv_events.go
  • deploy/operator/internal/enginemetadata/sglang/testdata/README.md
  • deploy/operator/internal/enginemetadata/sglang/testdata/model_info_generation.json
  • deploy/operator/internal/enginemetadata/sglang/testdata/models.json
  • deploy/operator/internal/enginemetadata/sglang/testdata/server_info_qwen3_huggingface_legacy_kv_events_config.json
  • deploy/operator/internal/enginemetadata/sglang/testdata/server_info_qwen3_huggingface_structured_kv_events.json
  • deploy/operator/internal/enginemetadata/sglang/types.go
  • deploy/operator/internal/enginemetadata/sglang/worker.go
  • deploy/operator/internal/enginemetadata/sglang/worker_test.go
  • deploy/operator/internal/selectionservice/client.go
  • deploy/operator/internal/selectionservice/client_test.go
  • deploy/operator/internal/selectionservice/types.go
  • docs/components/router/standalone-selection.md
  • docs/kubernetes/dynamo-operator.md
  • examples/README.md
  • examples/runtime-free-router/README.md
  • examples/runtime-free-router/sglang/README.md
  • examples/runtime-free-router/sglang/kustomization.yaml
  • examples/runtime-free-router/sglang/selection-service.yaml
  • examples/runtime-free-router/sglang/sglang-worker.yaml
  • lib/kv-router/src/services/selection/core/mod.rs
  • lib/kv-router/src/services/selection/tests.rs
  • lib/kv-router/src/services/selection/types.rs

Comment thread deploy/operator/internal/enginemetadata/sglang/kv_events.go Outdated
Comment thread deploy/operator/internal/enginemetadata/sglang/worker_test.go
Comment thread docs/components/router/standalone-selection.md Outdated
Comment thread examples/runtime-free-router/sglang/kustomization.yaml Outdated
@yankay yankay temporarily deployed to external_collaborator June 29, 2026 02:40 — with GitHub Actions Inactive
@yankay yankay temporarily deployed to external_collaborator June 29, 2026 08:33 — with GitHub Actions Inactive
@yankay yankay temporarily deployed to external_collaborator June 29, 2026 08:53 — with GitHub Actions Inactive
@yankay yankay force-pushed the feat/sglang-selection-adapter branch from 377757d to 7b2be42 Compare June 29, 2026 11:47
@yankay yankay temporarily deployed to external_collaborator June 29, 2026 11:48 — with GitHub Actions Inactive
@yankay yankay temporarily deployed to external_collaborator June 29, 2026 12:13 — with GitHub Actions Inactive
@waynehacking8

Copy link
Copy Markdown
Contributor

Ran the standalone-selection tests on this branch, all 24 pass including the new round-trip test. One semantics question on the PATCH path: apply() replaces the whole metadata map (types.rs, the new if let Some(metadata) block), so a second writer PATCHing just one key drops everything the operator wrote, and "metadata": {} silently clears the map while omitting the field leaves it untouched. If the map is meant to be operator-owned that's fine, but it'd be worth a line in the API docs saying PATCH replaces wholesale (or switching to a merge), plus a test pinning the {} -> clear behavior either way.

@yankay yankay temporarily deployed to external_collaborator July 6, 2026 02:27 — with GitHub Actions Inactive
@yankay

yankay commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Ran the standalone-selection tests on this branch, all 24 pass including the new round-trip test. One semantics question on the PATCH path: apply() replaces the whole metadata map (types.rs, the new if let Some(metadata) block), so a second writer PATCHing just one key drops everything the operator wrote, and "metadata": {} silently clears the map while omitting the field leaves it untouched. If the map is meant to be operator-owned that's fine, but it'd be worth a line in the API docs saying PATCH replaces wholesale (or switching to a merge), plus a test pinning the {} -> clear behavior either way.

HI @waynehacking8

Thanks for catching this, that is a good point. I kept the current whole-map replacement semantics for metadata, and added a docs clarification that {} clears it while omitting the field leaves it unchanged. I also added test coverage for the replace/omit/clear behavior.

@waynehacking8

Copy link
Copy Markdown
Contributor

Re-ran the suite at 63b85cb -- all green here (643 lib tests pass, including the 24 selection ones), and the new replace/omit/clear coverage plus the docs line are exactly what I was hoping for. Looks good from my side, thanks!

Signed-off-by: Kay Yan <kay.yan@daocloud.io>
yankay added 5 commits July 6, 2026 11:24
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
@yankay yankay force-pushed the feat/sglang-selection-adapter branch from 63b85cb to ba2e48a Compare July 6, 2026 03:29
@yankay yankay temporarily deployed to external_collaborator July 6, 2026 03:29 — with GitHub Actions Inactive
@yankay

yankay commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Re-ran the suite at 63b85cb -- all green here (643 lib tests pass, including the 24 selection ones), and the new replace/omit/clear coverage plus the docs line are exactly what I was hoping for. Looks good from my side, thanks!

Hi @waynehacking8
Thanks for re-running the suite and confirming. I just rebased the branch on the latest main, so the metadata patch coverage is now at the updated HEAD. I appreciate the careful review and the suggestion here :-)

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

Labels

deployment::k8s Relates to dynamo deployment in kubernetes documentation Improvements or additions to documentation external-contribution Pull request is from an external contributor feat size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants