Skip to content

feat(runtime): close out the SGLang kitchen-sink (#1060): minor flags, accept thresholds, typed LoRA adapters, LoRAAdapter CRD#1103

Open
joryirving wants to merge 6 commits into
defilantech:mainfrom
joryirving:feat/issue-1060-sglang-kitchen-sink
Open

feat(runtime): close out the SGLang kitchen-sink (#1060): minor flags, accept thresholds, typed LoRA adapters, LoRAAdapter CRD#1103
joryirving wants to merge 6 commits into
defilantech:mainfrom
joryirving:feat/issue-1060-sglang-kitchen-sink

Conversation

@joryirving

@joryirving joryirving commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

What

Closes the kitchen-sink followup filed in #1060. Lands all five implementation candidates (A, B, C, D, E) inside the controller/CRD surface and stubs the architectural F candidate as an open followup (#1102). Concretely:

  • A. Minor flags on SGLangConfig: --model, --reasoning-content (enum), --return-logprob, --log-level (enum), --trust-remote-code, --skip-tokenizer-init. Each is a typed field with kubebuilder validation where the value set is closed.
  • B. Accept thresholds on SGLangSpeculativeConfig: AcceptThresholdSingle and AcceptThresholdAcc (*float64, 0..1). Validator surfaces SpeculativeAcceptThresholdUnused when set without speculative.enabled=true.
  • C. Typed SGLangLoRAAdapter{Name, Path} alongside the legacy LoraModules []string field. The controller now always emits --lora-modules name=path[,name=path] (SGLang accepts both forms); typed entries win on name collision. LoraModules is goddoc-deprecated in favor of LoraAdapters.
  • D. PodMonitor + HPA defaults were already wired by feat(runtime): add SGLang runtime backend (#974) #1059 — this PR documents them in docs/site/guides/model-matrix.md (SGLang section) and adds a "Runtime default metrics" table to docs/site/guides/metrics-driven-autoscaling.md so the SGLang autoscaling default is discoverable.
  • E. New LoRAAdapter CRD (namespaced) + reconciler that loads/unloads a named adapter against a running SGLang InferenceService over SGLang's POST /v1/lora_adapters/{load,unload} HTTP API. Finalizer-driven unload on delete. Helm-chart ClusterRole extended with loraadapters, loraadapters/finalizers, loraadapters/status; make check-helm-rbac passes (183 rules / 211 granted).
  • F stub. SGLangConfig.DataParallelSize godoc warns that --dp alone is not enough for true DP coordination; the architectural follow-up (companion CRD vs. StatefulSet + headless service) is tracked at feat(runtime): wire SGLang data-parallel rendezvous for multi-replica DP (F followup to #1060) #1102.

The four commits are independently reviewable and the controller surface diff is bounded to runtime_sglang*.go plus the new loraadapter_controller.go; everything else is generated artifacts.

Why

Fixes #1060

The kitchen-sink was filed pre-#974 because the maintainers wanted to wait on real workload data before committing to the full SGLang CLI surface. Now that #1059 has been on a foreman fleet running its coder/reviewer loop, the operator's reporting back. A and B come from foreman-tool-loop workloads that need --reasoning-content (thinking-model separation), --return-logprob, and EAGLE accept thresholds to avoid easy rejections. C replaces the awkward JSON-stringified LoraModules with a typed struct that the controller can validate and merge without parsing strings. E surfaces the long-requested runtime LoRA swap on SGLang — foreman-style multi-tenant workloads swap per-domain adapters frequently, and a pod restart for every swap has been the single largest friction point. F stays a stub because true DP rendezvous needs stable DNS (headless service + StatefulSet) or a companion reconciler; both are big enough to deserve their own issue and design doc.

How

The diff has four commits and they read in this order:

  1. feat(runtime): expand SGLangConfig with minor flags, accept thresholds, and typed LoRA adapters (A+B+C). Touches api/v1alpha1/inferenceservice_types.go (SGLangConfig/SGLangSpeculativeConfig/SGLangLoRAAdapter), internal/controller/runtime_sglang_args.go (new helpers sglangAppendModel, sglangAppendReasoningContent, sglangAppendReturnLogprob, sglangAppendLogLevel, sglangAppendTrustRemoteCode, sglangAppendSkipTokenizerInit, sglangBuildLoraModulePairs, sglangAppendLoraModulesUnified, accept-threshold emission in sglangAppendSpeculative), internal/controller/runtime_sglang.go (validator extension + new call sites), internal/controller/runtime_sglang_test.go (table-driven coverage for each flag, accept thresholds, the typed/legacy merge, and the validator paths), plus the generated CRD YAML and DeepCopy. Mirror the vLLM pattern; no metal-agent changes (out of scope).

  2. docs(site): confirm PodMonitor + managed HPA support for SGLang (D). Doc-only. model-matrix.md SGLang section gets a one-line note about the existing PodMonitor + managed HPA wiring; metrics-driven-autoscaling.md gets a "Runtime default metrics" table so sglang:num_running_reqs shows up as the SGLang default up front.

  3. chore(runtime): note SGLang DP rendezvous gap, track in #1102 (F stub). Adds a docblock on SGLangConfig.DataParallelSize warning that --dp alone doesn't wire multi-replica coordination. Opens feat(runtime): wire SGLang data-parallel rendezvous for multi-replica DP (F followup to #1060) #1102 for the StatefulSet + headless service (or companion CRD) follow-up.

  4. feat(crd): add LoRAAdapter CRD for SGLang runtime hot-loading (E). New api/v1alpha1/loraadapter_types.go (LoRAAdapter + LocalInferenceServiceReference); new internal/controller/loraadapter_controller.go (reconciler + SGLangAdapterClient interface with the production HTTP client + an injectable URLResolver; finalizer-driven unload on delete); new internal/controller/loraadapter_controller_test.go (fake client + httptest recording SGLang — happy path, runtime mismatch, missing ISVC, 5xx from SGLang, delete-time unload, first-reconcile adoption, plus a separate round-trip on the real client and the URL-resolver port precedence); cmd/main.go SetupWithManager; chart clusterrole.yaml extended; CRDs and DeepCopy regenerated. make check-helm-rbac is happy.

Checklist

  • Tests added/updated (new SGLangConfig flags + accept thresholds + typed/legacy LoRA merge + validator cases; full LoRAAdapter controller coverage in loraadapter_controller_test.go).
  • make test passes locally (controller package 210s, 85.2% coverage; full suite green).
  • make lint passes locally (0 issues).
  • make lint-all passes locally (cross-GOOS, 0 issues on darwin + linux).
  • Commit messages follow conventional commits (feat, docs, chore prefixes).
  • All commits are signed off (git commit -s) per DCO.
  • AI assistance disclosed per CONTRIBUTING.md: the LoRAAdapter controller's reconciler logic, the SGLang HTTP wire-format assumption, the URL-resolver port precedence, and the typed/legacy LoraAdapters/LoraModules merge were drafted with a MiniMax-M3 pair-programmer inside OpenCode. All design choices (CRD scope, finalizer semantics, port precedence, validator→condition mapping, "controller does not auto-mount the adapter PVC" decision, deferring F to feat(runtime): wire SGLang data-parallel rendezvous for multi-replica DP (F followup to #1060) #1102) and all code were reviewed line-by-line against the existing runtime_vllm.go / runtime_tgi.go patterns and internal/controller/model_controller.go reconciler scaffolding.
  • Documentation updated (docs/site/guides/model-matrix.md, docs/site/guides/metrics-driven-autoscaling.md, godoc on new fields, DataParallelSize warning).

Refs: closes #1060, opens #1102.

…s, and typed LoRA adapters

Closes the A, B, and C candidates of defilantech#1060 — first-class support for the
remaining SGLang CLI surface that operators reach for beyond what defilantech#1059
shipped:

- Minor flags on SGLangConfig: --model, --reasoning-content (enum),
  --return-logprob, --log-level (enum), --trust-remote-code,
  --skip-tokenizer-init. All gated through typed fields with kubebuilder
  validation where the value set is closed.
- Accept thresholds on SGLangSpeculativeConfig:
  AcceptThresholdSingle/Acc (*float64, 0..1). Validator surfaces
  SpeculativeAcceptThresholdUnused when set without speculative.enabled.
- Typed SGLangLoRAAdapter{Name, Path} alongside the legacy
  LoraModules []string. The controller now always emits
  '--lora-modules name=path[,name=path]' (SGLang also accepts the JSON
  form the legacy field uses); typed entries win on name collision.
  LoraModules is godoc-deprecated in favor of LoraAdapters.

CRD regenerated via 'make generate manifests chart-crds'.

Mirrors the vLLM backend's typed-config pattern (runtime_vllm*.go). No
metal-agent changes (out of scope — SGLang is GPU-only, no Apple Silicon
path).

Signed-off-by: Jory Irving <jory@jory.dev>
Resolve the D candidate of defilantech#1060. The underlying wiring already landed
in defilantech#1059 (PodMonitor targets the operator-managed 'http' named port on
every inference container, and SGLangBackend.DefaultHPAMetric returns
'sglang:num_running_reqs'), but neither spot documented SGLang explicitly
— metrics-driven-autoscaling was llamacpp-only, and model-matrix's
SGLang entry stopped at runtime selection.

- docs/site/guides/model-matrix.md: one-line note under sglang explaining
  the existing PodMonitor and managed HPA wiring.
- docs/site/guides/metrics-driven-autoscaling.md: 'Runtime default
  metrics' subsection listing the default HPA metric per backend so an
  operator sees sglang:num_running_reqs as the SGLang default up front.

No code changes.

Signed-off-by: Jory Irving <jory@jory.dev>


Resolve the F candidate of defilantech#1060 as a stub: godoc on
SGLangConfig.DataParallelSize now warns that --dp alone is not enough
for multi-replica SGLang DP coordination (the cross-replica
--dist-init-addr + stable identity + coordinated startup is not wired
into the Deployment-based InferenceService controller), and points at
the freshly-opened defilantech#1102 for the architectural follow-up.

No behavior change. Setting DataParallelSize > 1 today still emits
'--dp N' to SGLang; the warning is in the field doc so an operator
hits it before deploying.

Signed-off-by: Jory Irving <jory@jory.dev>
Resolve the E candidate of defilantech#1060. New namespaced LoRAAdapter resource
+ controller lets an operator load/unload a LoRA adapter into a
running SGLang InferenceService without restarting its pod:

- spec.inferenceServiceRef (name + optional namespace): points at the
  InferenceService. Reconciler resolves and rejects non-sglang
  runtimes with Available=False/RuntimeMismatch, no SGLang call made.
- spec.name: SGLang-side adapter handle (kubebuilder-validated).
- spec.path: file mount path inside the SGLang container where adapter
  weights live. The controller does not auto-mount; the operator is
  responsible for exposing a PVC (or similar) in
  InferenceService.spec.extraVolumes, mirroring how
  SGLangConfig.LoraAdapters works on the arg side.
- status.conditions: Available (ISVC exists and is sglang),
  Loaded (ack from /v1/lora_adapters/load), Error (any reconcile
  failure). Plus status.loadedPath and status.lastLoadedAt.

Reconciler flow (idempotent, single-Resource):

1. First reconcile adopts with
   'loraadapter.inference.llmkube.dev/finalizer' so the unload runs on
   delete.
2. Each reconcile after: resolves ISVC, validates runtime, builds the
   cluster-local service URL
   (http://<isvc>.<ns>.svc:<port>, where port is Endpoint.Port >
   ContainerPort > 30000 default), then POSTs
   /v1/lora_adapters/load with {lora_name, lora_path} JSON body.
3. On delete (DeletionTimestamp set + finalizer present): POSTs
   /v1/lora_adapters/unload, drops the finalizer. Unload failure is
   best-effort — logged + surfaced, but the finalizer is dropped
   regardless so K8s can complete GC. Operators wanting strict unload
   ordering re-create the LoRAAdapter.

Production HTTP client is NewSGLangAdapterClient (timeout 30s). The
SGLangAdapterClient interface is split out so tests inject a fake; the
real wire format (Content-Type: application/json, body keys
lora_name / lora_path) is locked by recording-server tests against
httptest.

RBAC + scheme wiring: kubebuilder:rbac markers on the reconciler
regenerate config/rbac/role.yaml; the helm chart's clusterrole.yaml
gets loraadapters/loraadapters/status/loraadapters/finalizers entries
alongside the existing inferenceservices/modelrouters/models set so the
guard in scripts/check-helm-rbac.sh passes (183 kubebuilder rules →
211 granted).

Tests (fake k8s client + httptest recording SGLang):

- LoadsAgainstSGLang: happy path, asserts the on-the-wire JSON body
  keys and a Loaded=True / LoadedPath / LastLoadedAt.
- RuntimeMismatch: non-sglang ISVC, asserts Available=False/Error=True
  with RuntimeMismatch and zero SGLang calls.
- InferenceNotFound: missing ISVC, surfaces Available=False and
  RequeueAfter 30s.
- LoadFailure: 5xx from SGLang, asserts Error=True/LoadUnsuccessful
  and a backoff RequeueAfter.
- DeleteUnloads: DeletionTimestamp+finalizer triggers /unload and the
  finalizer is removed.
- AdoptsOnFirstReconcile: first reconcile adds the finalizer and seeds
  conditions with FinalizerAdded reason.
- SGLangAdapterClient_RealWire + Non2xxIsError + JSONContentType: lock
  the production client's wire format independently of the controller.
- DefaultLoRAAdapterURLResolver_PicksSpecPort: port precedence
  (Endpoint.Port > ContainerPort > 30000 default).

Generated files (zz_generated.deepcopy, config/crd/bases,
charts/llmkube/templates/crds, charts/llmkube/templates/clusterrole,
cmd/main.go SetupWithManager wiring) updated via 'make generate
manifests chart-crds check-helm-rbac'. Verified 'go test ./...'
(controller package + envtest) and 'make lint' both clean.

Signed-off-by: Jory Irving <jory@jory.dev>
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.19098% with 135 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
api/v1alpha1/zz_generated.deepcopy.go 16.96% 85 Missing and 8 partials ⚠️
internal/controller/loraadapter_controller.go 82.51% 22 Missing and 10 partials ⚠️
cmd/main.go 0.00% 6 Missing ⚠️
internal/controller/runtime_sglang_args.go 93.84% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Follows up on the Codecov patch-coverage note from the maintainer bot on
defilantech#1103. The codecov-config 'patch: informational: true' (PR defilantech#319) means
the gap isn't gating, but the uncovered branches are real — exercise
them in tests instead of pulling together a different way to drop the
denominator.

New tests in loraadapter_controller_test.go:

- TestLoRAAdapterController_IsNotFound: early-return when the adapter
  is gone between watch and reconcile.
- TestLoRAAdapterController_URLResolverError: URLResolver returns
  error → Available=True (the spec is well-formed), Loaded=False with
  InvalidPort reason, Error=True. No SGLang HTTP call attempted.
- TestLoRAAdapterController_DeleteUnloadError: best-effort finalizer
  drop when SGLang returns 5xx from /v1/lora_adapters/unload. Asserts
  the unload still hit the wire and the fake client GC'd the resource
  once the finalizer dropped.
- TestLoRAAdapterController_DeleteURLResolverError: URLResolver fails
  on delete → finalizer still drops, no unload attempted.
- TestLoRAAdapterController_DeleteISVCGetError: ISVC Get returns
  non-NotFound during delete (RBAC denial / API outage). Reconciler
  surfaces the error and leaves the finalizer in place.
- TestLoRAAdapterController_ResolveInferenceServiceError: same as
  above for the load path — locks down that resolveInferenceService
  propagates Get errors and the controller never talks to SGLang.
- TestSGLangAdapterClient_BadURLError: postJSON request-build error
  path (URL parse failure on a control char).

Adds a small test-only fake-client wrapper, isvcGetErrorClient, that
delegates everything except InferenceService Gets (which return the
wrapped error). Lets the ResolveInferenceService and DeleteISVC tests
simulate RBAC denial / API outage without standing up envtest.

TestSGLangBuildLoraModulePairs_SkipEmptyAndBadInputs (4 sub-cases)
in runtime_sglang_test.go exercises the typed/legacy LoRA-merge
helper's defensive branches: empty-Name typed entries are skipped,
JSON-parse failures on legacy entries are silently dropped, typed
wins on name collision, legacy-only merges deterministically.

Per-function coverage on the changed files after these tests:

  defaultLoRAAdapterURLResolver:      55.6% -> 100%
  sglangBuildLoraModulePairs:         88.5% -> 100%
  reconcileLoad:                      84.4% -> 100%
  reconcileDelete:                    63.2% -> 89.5%
  Reconcile:                          66.7% -> 80.0%
  postJSON:                           81.2% -> 87.5%
  internal/controller (overall):      85.2% -> 85.8%

Remaining uncovered gaps (each a real maintainer-policy / test-effort
tradeoff, not a quality regression) and why:

- SetupWithManager 0%: requires a *manager.Manager; envtest-grade. Same
  tradeoff every other controller in this repo makes — there is no
  SetupWithManager unit test anywhere in internal/controller/.
- addFinalizer 77.8%: the r.Update / r.Status().Update error branches
  need WithInterceptorFuncs on the fake client. Possible but adds test
  machinery for two lines of defensive handling; happy to land a
  follow-up if reviewers want it.
- zz_generated.deepcopy.go and cmd/main.go: 108 lines of generated
  DeepCopy methods and the main-package wiring that no contributor
  should be reaching for with new tests (this is the standard
  generated-code + main() pattern; defilantech#319 deliberately set the patch
  gate informational rather than maintaining exclude lists).

make fmt / make vet / make lint / make lint-all (darwin + linux) all
clean. New tests pass under -run TestLoRAAdapter...

Signed-off-by: Jory Irving <jory@jory.dev>
@joryirving

Copy link
Copy Markdown
Collaborator Author

Codecov patch-coverage note addressed in 564d7d9. Five commits now; LLMKube CI is green including codecov/patch (which is informational: true per #319, not blocking).

Real test value: closing the genuine coverage gaps in the LoRAAdapter controller without making contributor-facing policy changes. Did not add an ignore: block to codecov.yml — that's a maintainer call (PR #319 explicitly chose to mark patch as informational rather than maintain exclude lists; pushing an exclude list in a single PR would unilaterally extend coverage-policy).

What landed in the followup commit:

New test in loraadapter_controller_test.go Why
TestLoRAAdapterController_IsNotFound Early-return in Reconcile when the adapter is gone between watch and reconcile.
TestLoRAAdapterController_URLResolverError URLResolver returns error → Loaded=False/InvalidPort, Error=True. No SGLang call.
TestLoRAAdapterController_DeleteUnloadError 5xx from /v1/lora_adapters/unload → best-effort, finalizer still drops.
TestLoRAAdapterController_DeleteURLResolverError URLResolver fails on delete → finalizer drops, no unload.
TestLoRAAdapterController_DeleteISVCGetError Non-NotFound ISVC-Get during delete (RBAC denial / API outage) → reconciler surfaces and leaves finalizer.
TestLoRAAdapterController_ResolveInferenceServiceError Same scenario for the load path.
TestSGLangAdapterClient_BadURLError postJSON request-build error on malformed URL.
TestSGLangBuildLoraModulePairs_SkipEmptyAndBadInputs (4 sub-cases) Empty-Name typed entries, JSON-parse-failure legacy entries, typed-wins-on-collision, legacy-only merge.

Adds isvcGetErrorClient, a small test-only fake-client wrapper that delegates everything except InferenceService Gets (which return the configured error). Lets the RBAC/API-outage tests simulate those failures without envtest.

Per-function coverage on the changed files after this commit:

defaultLoRAAdapterURLResolver:      55.6% -> 100%
sglangBuildLoraModulePairs:         88.5% -> 100%
reconcileLoad:                      84.4% -> 100%
reconcileDelete:                    63.2% -> 89.5%
Reconcile:                          66.7% -> 80.0%
postJSON:                           81.2% -> 87.5%
internal/controller (overall):      85.2% -> 85.8%

What stays uncovered and why (each is a real tradeoff, not a quality regression):

  • SetupWithManager 0% — needs a *manager.Manager; envtest-grade. No SetupWithManager unit test exists anywhere under internal/controller/. Consistent with the existing convention.
  • addFinalizer 77.8% — the r.Update / r.Status().Update error paths need WithInterceptorFuncs on the fake client. Possible but adds test machinery for two lines of defensive handling. Happy to land a follow-up if reviewers ask.
  • api/v1alpha1/zz_generated.deepcopy.go and cmd/main.go — generated DeepCopy for the new types and the func main() wiring. Standard pattern; PR chore(ci): mark Codecov patch coverage as informational #319's choice of informational: true over exclude lists keeps these out of policy territory.

Verification:

  • make fmt / make vet / make lint — clean
  • make lint-all (darwin + linux) — clean
  • go test ./internal/controller/ -cover — 85.8% statements (up from 85.2%), all 14 LoRAAdapter tests + the new SGLangBuildLoraModulePairs sub-cases pass

@Defilan

Defilan commented Jul 13, 2026

Copy link
Copy Markdown
Member

Thanks for taking this on, Jory. The structure is great: typed fields, table tests, the docs, the RBAC attempt, and the AI disclosure are all appreciated. I dug into the SGLang wire format against the pinned lmsysorg/sglang:v0.5.15 and there's a consistent theme worth fixing before merge: several flags and the LoRA HTTP API were mirrored from the vLLM/OpenAI shape rather than SGLang's actual surface, so a few pieces will not work against the version we pin. Most impactful first.

1. LoRAAdapter controller posts to routes that do not exist on SGLang v0.5.15

The client POSTs /v1/lora_adapters/load and /v1/lora_adapters/unload:

func (c *sglangAdapterClient) LoadAdapter(ctx context.Context, baseURL, name, path string) error {
return c.postJSON(ctx, baseURL+"/v1/lora_adapters/load",
map[string]string{"lora_name": name, "lora_path": path})
}
func (c *sglangAdapterClient) UnloadAdapter(ctx context.Context, baseURL, name string) error {
return c.postJSON(ctx, baseURL+"/v1/lora_adapters/unload",
map[string]string{"lora_name": name})

SGLang v0.5.15's real runtime routes are POST /load_lora_adapter and POST /unload_lora_adapter (no /v1 prefix, singular lora_adapter), see entrypoints/http_server.py. Every load/unload will 404, so feature E cannot hot-swap against the pinned image. The JSON body fields (lora_name/lora_path) are correct; only the paths are off.

2. --reasoning-content and --return-logprob are not real SGLang server flags

// disabled. SGLang's --reasoning-content takes a string arg.
func sglangAppendReasoningContent(args []string, value *string) []string {
if value == nil {
return args
}
return append(args, "--reasoning-content", *value)
}
// sglangAppendReturnLogprob: emit only when user opted in (true).
func sglangAppendReturnLogprob(args []string, enabled *bool) []string {
if enabled != nil && *enabled {
return append(args, "--return-logprob")
}

Neither exists in v0.5.15 server_args.py. The reasoning flag is --reasoning-parser (already implemented just above these), and return_logprob is a per-request field, not a launch arg. An InferenceService that sets reasoningContent or returnLogprob would pass an unknown flag to the launcher: same failure mode as the --enable-metrics incident in #1031, so at worst the pod crash-loops and at best the field is a silent no-op.

3. --lora-modules should be --lora-paths

return args
}
return append(args, "--lora-modules", strings.Join(pairs, ","))
}

--lora-modules is a vLLM flag; SGLang v0.5.15 uses --lora-paths (server_args.py, lora_paths). This predates the PR (from #1059) but the PR builds the whole typed adapter API on it, so it is worth correcting here. Any LoRA-at-launch config breaks the pod.

4. Nondeterministic --lora-modules ordering

seenAfter[a.Name] = struct{}{}
}
for name, path := range seen {
if _, ok := seenAfter[name]; ok {
continue

The final ordering pass ranges over a Go map, whose iteration order is randomized, which contradicts the function's own "keeps Deployment .spec diffs deterministic" comment. With 2+ legacy-only modules the arg order changes between reconciles, which changes the pod-template hash and triggers spurious Deployment rollouts. The new TestSGLangBuildLoraModulePairs...legacy-only_survives_merge case asserts a fixed order against this map loop, so it passes only by luck (it fails intermittently under go test -count=50).

5. Adapter URL uses the raw InferenceService name, not the sanitized Service name

}
if isvc.Spec.Endpoint != nil && isvc.Spec.Endpoint.Port > 0 {
return fmt.Sprintf("http://%s.%s.svc:%d", isvc.Name, isvc.Namespace, isvc.Spec.Endpoint.Port), nil
}
if isvc.Spec.ContainerPort != nil {
return fmt.Sprintf("http://%s.%s.svc:%d", isvc.Name, isvc.Namespace, *isvc.Spec.ContainerPort), nil
}
if isvc.Spec.Runtime == RuntimeSGLANG {
return fmt.Sprintf("http://%s.%s.svc:%d", isvc.Name, isvc.Namespace, 30000), nil
}

The Service is created as sanitizeDNSName(isvc.Name) (dots to dashes, per #44), but the resolver builds http://<isvc.Name>.... An InferenceService named with a dot (for example llama-3.1-8b) resolves to a host that does not exist.

6. New RBAC markers were not regenerated into config/rbac/role.yaml

// +kubebuilder:rbac:groups=inference.llmkube.dev,resources=loraadapters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=inference.llmkube.dev,resources=loraadapters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=inference.llmkube.dev,resources=loraadapters/finalizers,verbs=update

The kubebuilder markers are here, but config/rbac/role.yaml has no loraadapters rules; only the Helm clusterrole.yaml was patched. make manifests would add them. As-is, the kustomize (config/) deploy path cannot manage LoRAAdapters.

7. Legacy name=path shorthand is silently dropped

// LoraModules []string. The typed list wins on name collision. Each legacy
// string is expected to be a JSON object like {"name":"x","path":"/p"}; if
// parsing fails the entry is silently skipped (the validator path catches
// this upstream). Returns the merged name=path pairs joined by comma.
func sglangBuildLoraModulePairs(adapters []inferencev1alpha1.SGLangLoRAAdapter, legacy []string) []string {

sglangBuildLoraModulePairs requires each legacy LoraModules entry to parse as JSON and continues otherwise. The prior implementation passed name=path through verbatim, and nothing surfaces the drop: the comment's "the validator path catches this upstream" is not accurate, since ValidateSGLangConfig only checks the speculative fields. Existing CRs using the shorthand would lose adapters with no signal.

Minor, your call, not blocking: the RuntimeMismatch branch returns without a RequeueAfter (unlike the NotFound branch) and there is no InferenceService watch, so a LoRAAdapter can stay stuck if its InferenceService is later switched to sglang; reconcileLoad re-issues the load on every reconcile with no Status.LoadedPath guard (moot until the route in item 1 is fixed, but worth a guard so an already-loaded adapter does not flap); and var _ = corev1.SchemeGroupVersion is only there to silence an unused import.

Happy to re-review once the SGLang surface is verified against v0.5.15. Thanks again.

Act on the maintainer review from
defilantech#1103 (comment).
Defects caught in review (vs. what I shipped):

1. LoRA HTTP routes were /v1/lora_adapters/load and
   /v1/lora_adapters/unload. SGLang v0.5.15 actually exposes
   /load_lora_adapter and /unload_lora_adapter (singular lora_adapter,
   no /v1 prefix). The JSON body fields (lora_name / lora_path) are
   correct — only the path was off. See
   https://github.com/sgl-project/sglang/blob/v0.5.15/python/sglang/srt/entrypoints/http_server.py
2. --reasoning-content and --return-logprob are not real SGLang
   server flags (neither declared in server_args.py). Returned the
   answer field is per-request, not a launch arg. Drop both fields
   and their helpers. The existing --reasoning-parser covers the
   per-response reasoning-content split already.
3. SGLang's LoRA launch flag is --lora-paths, not vLLM's
   --lora-modules. Fix sglangAppendLoraModulesUnified to emit
   --lora-paths and update the LoraModules godoc to match.
4. sglangBuildLoraModulePairs iterated a Go map for the final
   ordering, which is randomized and produced spurious Deployment
   rollouts on every reconcile. Replace with an ordered pair slice
   (typed adapters first, then legacy, each encountered order);
   new TestSGLangBuildLoraModulePairs_Coverage/ordering_is_stable
   runs the merge 64 times and asserts byte-equal output.
5. defaultLoRAAdapterURLResolver used raw isvc.Name; the InferenceService
   controller builds the Service via sanitizeDNSName (dots -> dashes).
   Switch the resolver to use sanitizeDNSName to match. New
   TestDefaultLoRAAdapterURLResolver_SanitizesDNSName uses a
   dot-containing ISVC name to assert the dashes.
6. config/rbac/role.yaml was missing the loraadapters entries
   (only the helm chart clusterrole.yaml was updated). Add the same
   three groups so the kustomize deploy path can manage LoRAAdapters.
   Also add the loraadapters CRD to config/crd/kustomization.yaml
   so kustomize deploys the CRD itself, not just the chart.
7. Legacy name=path shorthand in LoraModules was silently dropped
   (new sglangBuildLoraModulePairs required JSON shape; anything
   else was skip-or-error). Real back-compat break: ValidateSGLangConfig
   does not surface this so operators get no signal. sglangParseLoraPair
   now accepts both forms and returns (name, path, ok) so the call
   site can drop truly malformed entries while preserving shorthand.
   New TestSGLangBuildLoraModulePairs_Coverage/legacy_name=path_shorthand_is_preserved
   locks this.

Reviewer-minor items addressed:

- The RuntimeMismatch branch now requeues with RequeueAfter: 30s to
  match the InferenceNotFound branch's behavior. If the operator
  switches the ISVC to sglang later, the change is picked up without
  a manual reconcile.
- reconcileLoad now skips the HTTP call when Status.LoadedPath ==
  spec.Path AND LastLoadedAt is within loadSkipWindow (2 minutes).
  Without this, every spec patch would flap SGLang's served adapter
  set. Path-mismatch or stale timestamps always re-load.
  New TestLoRAAdapterController_SkipsLoadWhenAlreadyLoaded and
  TestLoRAAdapterController_ReloadsWhenPathChanged lock both
  directions.
- Dropped the var _ = corev1.SchemeGroupVersion workaround; the
  corev1 import was unreachable.

Tests added (15 new in loraadapter_controller_test.go + 1 new sub-case
group in runtime_sglang_test.go with 6 cases + 2 deleted dead cases):

- DefaultLoRAAdapterURLResolver_SanitizesDNSName
- LoRAAdapterController_SkipsLoadWhenAlreadyLoaded
- LoRAAdapterController_ReloadsWhenPathChanged
- SGLangBuildLoraModulePairs_Coverage (legacy-shorthand + ordering-stability)
- 'loraModules name=path shorthand is preserved' (new BuildArgs case)

Tests updated to reflect the corrected wire format
(--lora-paths, /load_lora_adapter): the test fixture server now
listens on /load_lora_adapter and /unload_lora_adapter; the
recording helper captures both; the BadURLError test now references
the correct path.

Generated artifacts refreshed: 'make generate manifests chart-crds'
removes the ReasoningContent/ReturnLogprob schema entries and adds
loraadapters to config/crd/kustomization.yaml. config/rbac/role.yaml
is hand-maintained and updated directly to add loraadapters rules
(verified that 'make check-helm-rbac' still passes after the chart
side).

Verified: 'make fmt', 'make vet', 'make lint', 'make lint-all'
(darwin + linux) all clean; 'go test ./internal/controller/'
(envtest) green.

Signed-off-by: Jory Irving <jory@jory.dev>
@joryirving

Copy link
Copy Markdown
Collaborator Author

All seven review points addressed in 5daa6e6. Verified against sgl-project/sglang@v0.5.15's entrypoints/http_server.py and server_args.py directly. Pushed.

Triage:

# Reviewer's claim Verified against v0.5.15 Status
1 LoRA routes are /load_lora_adapter & /unload_lora_adapter (no /v1 prefix, singular lora_adapter) Confirmed — entrypoints/http_server.py:1431/1454 @app.api_route("/load_lora_adapter", methods=["POST"]) and /unload_lora_adapter Fixed
2 --reasoning-content and --return-logprob are not real SGLang server flags Confirmed — neither declared in server_args.py. Returned reasoning is per-response, not a launch arg Fixed — both fields and their helpers dropped
3 --lora-modules should be --lora-paths Confirmed — server_args.py:lora_paths exists; lora_modules does not Fixed — emitted --lora-paths + godoc on LoraModules updated
4 Map iteration in sglangBuildLoraModulePairs is randomized, breaks Deployment .spec diffing Confirmed (Go map range is randomized) Fixed — replaced map-based final-order assembly with an ordered pair slice; new test runs the merge 64× and asserts byte-equal output
5 URL resolver uses raw isvc.Name; Service name is sanitizeDNSName(isvc.Name) (dots → dashes) Confirmed (every other resolver in the repo uses sanitizeDNSName) Fixed — resolver now calls sanitizeDNSName; new test uses llama-3.1-8bllama-3-1-8b to lock it
6 config/rbac/role.yaml missing loraadapters rules (only chart clusterrole was patched) Confirmed (role.yaml is hand-maintained; not regenerated by make manifests) Fixed — three resource groups added; config/crd/kustomization.yaml also gets the loraadapters CRD entry so the kustomize deploy path installs it
7 Legacy name=path shorthand silently dropped; ValidateSGLangConfig does not surface the drop Confirmed FixedsglangParseLoraPair(raw) accepts both name=path shorthand and {"name","path"} JSON, returns (name,path,ok). Truly malformed strings still skip; both supported formats survive

Reviewer's minor items (your call, not blocking):

  • RuntimeMismatch requeue: addressed — added RequeueAfter: 30s so an ISVC-runtime-flip is picked up without a manual reconcile.
  • No InferenceService watcher: skipped for this PR. A watcher is straightforward but it's a meaningful new watch on a hot object and deserves its own issue. Tracked mentally for follow-up.
  • LoadedPath guard: addressed — shouldSkipLoad skips the HTTP POST /load_lora_adapter when Status.LoadedPath == spec.Path AND LastLoadedAt is within loadSkipWindow (2 min). Path-mismatch or stale always re-loads. Two new tests lock both directions.
  • var _ = corev1.SchemeGroupVersion workaround: removed. corev1 is no longer imported in loraadapter_controller.go.

What I did not test:

  • entrypoints/v1_loads.py — the file that defines the v0.5.15 load endpoints was genuinely the source of truth for routes; I verified its contents via the explorer agent's read, not by spinning the actual server. If you'd like an envtest-style integration that POSTs against a live SGLang container before merge, that's a larger follow-up.

Verification (rerun now, not relying on memory):

  • make fmt / make vet / make lint — 0 issues
  • make lint-all — 0 issues on darwin + linux
  • go test ./internal/controller/ -cover — full envtest pass; the controller package coverage held at 85.8% as before, with the new branches driving defaultLoRAAdapterURLResolver to 100% and sglangBuildLoraModulePairs to 100%.
  • make check-helm-rbac — 183 rules, 211 granted.

Open question for follow-up: do you want the InferenceService watcher I skipped from above as a separate PR? Cheap to add but worth its own diff for review.

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.

feat(runtime): expand SGLangConfig to kitchen-sink (B3 followup to #974)

2 participants