feat: add multi-node serving and researcher role alias - #269
Pengfei Ni (feiskyer) wants to merge 6 commits into
Conversation
yliu382
left a comment
There was a problem hiding this comment.
Summary
Adds multi-node Ray Serve (CPU head + fixed GPU worker pool) driven by workload-profile workerCount/gpusPerWorker/placement, plus two auxiliary flags (--app-args for builder-owned Ray Serve apps, --workload-profile-snapshot for offline --dry-run=client rendering), and renames the default researcher role to researcher while preserving tau-researcher-v1 as a backward-compatible alias across CRD enum, CLI, workspace connection descriptor, verifier, adoption compare, and reconciler. Coverage is unusually thorough for a change of this size and I did not find a functional or security regression.
Test Plan & Verification Results
Executed against the PR HEAD (51642e5) checked out under /home/vibe/taugrid:
- Build: ✅
go build ./...on bothcli/andcontrollers/tau-core/clean. - Unit tests (
controllers/tau-core): ✅go test ./api/... ./internal/...all pass, including the newTestWorkspaceRoleAliasIsBackwardCompatible-style loop that verifies switchingspec.rolebetweenresearcherandtau-researcher-v1produces byte-identicalRoleBinding/ClusterRoleBinding/Rolelists. - Unit tests (
cli): ✅internal/serve,internal/workspace,internal/workspaceconnection,internal/reposcaffold.⚠️ 1 failure ininternal/cli:TestLoadServeAppArgsRejectsInvalidInput/non-string_nested_key— the reviewer sandbox ships Go 1.27.1 andGOTOOLCHAIN=autodid not fetch the pinnedgo1.26.7, soencoding/jsonnow stringifies non-string interface keys instead of failing. Not a PR bug (author's CI runs on the pinned toolchain), but see the note in Findings. - Lint/vet: ✅
go vet ./...clean on both modules. - CRD sync check: ✅
md5summatches betweencharts/tau-core-controller/crds/tau.azure.com_workspaces.yamlandcontrollers/tau-core/config/crd/bases/tau.azure.com_workspaces.yaml. - Cluster deployment / e2e: Skipped for this PR — it is a CLI + CRD schema change, not a chart/controller behavior change, and there is no reachable cluster surface that would exercise the new
--nodes/--shm-size/--app-argsrender paths without also standing up a full KubeRay + Kueue stack. Behavioral coverage is instead exercised by the newTestRenderDistributedRayService,TestServeSnapshotProvenanceOnDeploymentChildren, and the eight-rank snapshot fixture tests — all pass. - Regression check: ✅ Full unit test suites of
cliandcontrollers/tau-corepass (the single failure above excepted).
Security Scan
- Secrets/tokens: ✅ none introduced.
HF_TOKENin tests usesenvspec.Secret(...)valueFrom. - Privileged/hostNetwork/hostPID/capabilities: ✅ none added.
- RBAC changes: ✅ no new verbs or resources. The
researcheralias binds the existingtau-researcher-v1ClusterRole — no permission surface change (asserted byTestKubectlVerifierResearcherRoleCompatibility/alias cannot bypass permissionsand by the RBAC-parity assertions incontroller_test.go). - Container images: ✅ n/a — no image references changed.
- Network exposure: ✅ no new Services / Ingress / removed NetworkPolicies. Ray Serve
http_options.host: 0.0.0.0was already the shape; only the port line is now emitted explicitly. - Snapshot boundary: ✅ good defence —
resolveSnapshotServeProfilerejects--dry-run=server, empty apply, or--contextoverrides; snapshot mode never touches the cluster (Restore: func() {}), andTestServeSnapshotCannotAuthorizeLiveOperationslocks that down.
Breaking Changes
- CRD enum broadened, not narrowed (
enum: [tau-researcher-v1] → [researcher, tau-researcher-v1]): existingTauWorkspaceobjects withrole: tau-researcher-v1remain valid. New CRD must be installed before CLI-generated objects withrole: researcherare applied — this is explicitly called out incli/README.mdL102-107. Good. - Default researcher role in newly-generated artefacts changes (
create,reposcaffold, workspace connection descriptor) fromtau-researcher-v1→researcher. Adoption comparison insameAdoptionIntenttreats both as equal, andKubectlVerifieraccepts either side, so re-runningtau workspace createagainst a live workspace that still saystau-researcher-v1is a no-op. Verified inTestResearcherRoleCompatibility. selectServeWorkloadProfilesignature changed (addedexplicitNodes *int, kind string). Only internal callers; no exported break.- Ray Serve config now emits
http_options.host/port, andRenderDeployment/Ray render swapped a customyamlWriterforstrings.Builder(identical output). Verified by the surrounding regression tests. - No proto, no gRPC, no downstream ConfigMap/Secret shape change.
Findings
Nit — serve.go L163-164, --nodes upper bound. The check int64(nodes) > 2147483647 compares an int (a value that Go already caps at math.MaxInt64 on 64-bit) after casting to int64. On 64-bit hosts this bound only triggers when someone passes an explicit value > 2^31-1, which is fine, but the intent (Workers is int32 downstream, so we need to fit in an int32) reads more clearly as nodes > math.MaxInt32. Not blocking.
Observation — render.go L168-171. copyResources(p.Resources.Requests) shallow-copies the top-level map; nested values (e.g. quantities as resource.Quantity) still share references. The tests confirm the caller's profile isn't mutated for the current render paths, but if a future callsite mutates a nested map[string]any from a resource entry the aliasing would surface. Consider using a deep copy helper (or runtime.DeepCopyJSON-style) if that ever becomes possible. Not blocking.
Observation — Go toolchain drift. The single failing non-string_nested_key case (see Test Plan) is a Go 1.27 stdlib behavior change (encoding/json now marshals map[interface{}]interface{}{1: "value"} as {"1":"value"} instead of erroring). This test asserts on the pre-1.27 error path. Author's CI on the pinned 1.26.7 will pass, but this test will start failing once the repo bumps .go-version to any release ≥ 1.27. Cheap follow-up: assert against actual invalid inputs (e.g. func(){}, chan int) that encoding/json still rejects. Not blocking this PR.
Positive callouts:
- The failure-mode matrix in
TestServeSnapshotFailsClosed(namespace required, context conflict, scope mismatch, GPU/node conflict, deployment cardinality, unavailable profile, startup args, hash tamper) is a great template for future offline-render features. TestRenderDistributedRayServiceexplicitly asserts head has no GPU tolerations, no GPU resource requests, no worker TAS annotations, and workers carrymatchLabelKeys: [ray.io/cluster]for anti-affinity across rollouts. Exactly the right invariants.- Snapshot provenance annotation
tau.azure.com/workload-profile-source: snapshotpropagates to child Deployment/Service/HPA (TestServeSnapshotProvenanceOnDeploymentChildren) — makes offline-rendered artefacts trivially auditable.
Verdict
APPROVE. No hard-block conditions met. Build, vet, and unit tests pass under both modules; RBAC-parity, snapshot-safety, and CRD-sync invariants are covered by new tests; description matches code; alias migration is documented in cli/README.md.
gossion
left a comment
There was a problem hiding this comment.
Review found one blocking permission issue.
yliu382
left a comment
There was a problem hiding this comment.
Summary
Re-review of the delta since my previous APPROVE (51642e5 → 9fd79e4). The two new commits fill a natural gap left by the earlier multi-node-serving work: the researcher role and CLI connection verifier now also cover RayService alongside RayJob, plus new tests and kind-e2e coverage. Delta is small (10 files, +142/-5), focused, and well tested. Approving.
Test Plan & Verification Results
Focused on the delta commits (0be8c24 grant researcher access, 9fd79e4 controller smoke fixtures) rather than re-running everything on the previously approved base.
- Build: ✅
go build ./...in bothcli/andcontrollers/tau-core/ - Unit tests: ✅
cli/internal/workspaceconnection— passes (covers the newTestKubectlVerifierRejectsMissingRayServicePermissionmatrix acrossresearcher/tau-researcher-v1× create/get/list/patch/delete, and the existing role-compat suite now includingrayservices.ray.io)controllers/tau-core/internal/controller— passes (TestKustomizeResearcherRoleGrantsRayServicePermissionsdecodes the kustomizerbac.yaml, finds thetau-researcher-v1ClusterRole, and asserts the exact RayJob+RayService rule)
- Lint/vet: ✅
go vetclean in touched packages - Helm: ✅
helm lint charts/tau-core-controllercleanhelm unittest --strict charts/tau-core-controller— all 10 tests pass, including newResearcher RayService permissionssuitekubectl kustomize charts/tau-core-controllerrenders cleanly
- Cluster deployment: skipped — taugrid is not deployed to the aks-ai-runtime reviewer cluster; kind-e2e.sh boundary checks are inspected statically
- Behavioral verification: verifier now issues
kubectl auth can-i {create,get,list,patch,delete} rayservices.ray.ioin the workspace namespace, in the same style as the existing RayJob checks. kind-e2e.sh installs a mockrayservices.ray.ioCRD, waits forcondition=Established(nice hardening — previously CRDs were applied without a readiness gate), and boundary-checks that all seven RayService verbs areyesin the target namespace andnoin the system namespace. - Regression: ✅ No changes outside the RayService-permission surface; previously validated multi-node-serving behavior,
--app-args,--workload-profile-snapshot, andresearcher/tau-researcher-v1alias behavior are untouched.
Security Scan
- Secrets/tokens: ✅ None
- Privilege escalation: ✅ No new
privileged/hostNetwork/hostPID/ added capabilities - RBAC changes:
⚠️ (justified) —tau-researcher-v1ClusterRole extended to includerayservicesin the same rule that already grantedrayjobs(create,get,list,watch,delete,patch,update). This is bound via the existing per-workspace RoleBinding (namespace-scoped), not cluster-wide, so the effective grant is scoped to a workspace's target namespace and follows the established pattern. Justified in the PR description and required by the multi-node RayService serving feature merged earlier in this PR. - Container images: ✅ No image changes
- Secret handling: ✅ No secret env/volume changes
- Network exposure: ✅ No Service/Ingress/NetworkPolicy changes
Breaking Changes
- Workspace RBAC contract change:
KubectlVerifier.Verifynow requires create/get/list/patch/delete onrayservices.ray.io. Any pre-existing workspace that hasn't upgraded the Helm/Kustomize RBAC will fail connection verification with the new CLI. This is clearly and repeatedly documented (PR body,cli/README.md,site/content/en/docs/developer-guide/serve-model.md,site/content/en/docs/platform-admin-guide/setup-guides/handoff.md), with explicit upgrade instructions ("Install the updated Helm or Kustomize RBAC before connecting with this CLI"). Migration guidance is sufficient — not a hard block.
Findings
Nothing blocking. A few small observations:
controllers/tau-core/internal/controller/workspace_rbac_test.gouses a repo-relative path (../../../../charts/tau-core-controller/kustomize/rbac.yaml) to load the kustomize file. This is fine because Go tests run withpwd == package dir, but it does couple the controller test to the repo layout — noting for future refactors, not blocking.- Nice touch:
kind-e2e.shnowkubectl wait --for=condition=Establishedon the mock workload CRDs. This closes a real race window where the RESTMapper could reject subsequentauth can-icalls againstrayservices.ray.iobefore the CRD was fully registered. Recommend keeping this pattern for any future mock CRDs added to the script. - The new verifier subtest matrix (
role × verb) explicitly proves that either role alias enforces every RayService permission — great coverage against future silent regressions in the alias-compat path.
Verdict
APPROVE — the delta is a clean, well-tested extension of the RayJob RBAC pattern to RayService, matching the multi-node RayService serving feature already approved in this PR. All local checks (build, unit, vet, helm lint/unittest, kustomize render) pass, the upgrade requirement is thoroughly documented, and the RBAC change stays inside the existing workspace-scoped RoleBinding boundary.
Li Chen (AaronLiChen)
left a comment
There was a problem hiding this comment.
Reviewed the current head. The new unit/CLI tests cover rendering, snapshot validation, app args, role aliases, and RBAC. The Kind controller smoke covers the logical role and RayService auth can-i checks, but it uses a permissive mock RayService CRD and does not create a RayService. The runtime Kind smoke installs real KubeRay/Kueue but exercises only Job and RayJob. Thus there is still no end-to-end coverage for RayService admission, generated head/worker Pods, custom ports, shared memory, or readiness.
yliu382
left a comment
There was a problem hiding this comment.
Summary
Re-review of the delta since my previous APPROVE (9fd79e4 → d6c2c7f). The single new commit "Fix RayService Contracts and Upgrade Tau CRDs" is a focused, well-tested extension of the multi-node serving work: (a) the CLI now upgrades tau.azure.com CRDs from the selected chart before helm upgrade, (b) the distributed Ray renderer publishes the Serve port on workers, adds a CriticalAddonsOnly toleration on the CPU head, and propagates topology labels/pod-priority to the head, and (c) the kind smoke script now exercises RayService end-to-end and the legacy → aliased researcher CRD upgrade. Delta is 16 files, +564/-24. Approving.
Test Plan & Verification Results
Focused on the delta (d6c2c7f) — previous base already approved.
- Build: ✅
go build ./...incli/clean. - Unit tests: ✅
cli/internal/cli—ok(0.32s). Covers newTestClusterInstallUpgradesCRDsBeforeExistingRelease(validates render → server-dry-run → apply → wait order, plus failure propagation at each step),TestTauGridCRDManifestExcludesWorkloadsAndThirdPartyCRDs(filters outrayservices.ray.ioand user CRs),TestKindSmokeExercisesRayServiceAndLegacyCRDUpgrade, andTestServeRayWorkloadPriority(workspace + pod priority separation).cli/internal/serve—ok(0.02s). CoversTestRenderKindRayServiceFixture(offline fixture generation) andTestDistributedRayServicePorts(Serve port matches on head, workers, andserveConfigV2.http_options.portfor both 8000 and 9000; workers do not advertisedashboard/gcs-server).
- Lint/vet: ✅
go vet ./...clean oncli/. - Cluster deployment: skipped — CRD upgrade path is a pre-
helm upgradestep that runs against a live cluster with tau-core-controller Helm state, which isn't installed on the aks-ai-runtime reviewer cluster. The kind-smoke script is inspected statically and asserted viaTestKindSmokeExercisesRayServiceAndLegacyCRDUpgrade. - Behavioral verification (from tests + code reading):
upgradeTauGridCRDsrenders the exact chart+version+values used for install (clusterInstallRenderArgs), filters manifest toapiextensions.k8s.io/v1CRDs in grouptau.azure.com, runskubectl apply --dry-run=serverfirst, thenkubectl apply --field-manager=taugrid-crds, thenkubectl wait --for=condition=Established -f -withspec.Timeout. Existing CRs are retained (no--prune). Third-party CRDs (rayservices.ray.io, kueue, etc.) are explicitly excluded, and non-CRD tau resources likeTauWorkspaceare filtered by kind check.- Head pod
tolerationsnow[{key: CriticalAddonsOnly, operator: Exists, effect: NoSchedule}], matching the AKS system pool taint pattern already used elsewhere. GPU-only tolerations remain worker-only. Options.ServePortis now honored on both head and workers, matchingserveConfigV2.http_options.port. Worker containers keep only theserveport;dashboard(8265) andgcs-server(6379) remain head-only.render.gonow mergestopoPlan.Labelsinto RayService labels and setspriorityClassNameon the head pod spec when the topology plan supplies one — the two-level (workload vs pod) priority split is validated byTestServeRayWorkloadPriority.
- Regression: ✅ Previously validated behavior — multi-node-serving contract,
--app-args,--workload-profile-snapshot,researcher/tau-researcher-v1alias, and RayService RBAC — is untouched. The head-toleration change is intentional (previous test asserted no tolerations; it is updated in the same commit to assert the newCriticalAddonsOnlytoleration).
Security Scan
- Secrets/tokens: ✅ None.
- Privilege escalation: ✅ No new
privileged/hostNetwork/hostPID/capabilities. The head toleration forCriticalAddonsOnly:NoScheduleis a scheduling hint, not a privilege escalation. - RBAC changes: ✅ None in this delta.
taugrid-crdsis used as a Server-Side-Apply--field-manager, not a Kubernetes principal. - Container images: ✅ No image changes. Fixture references
mcr.microsoft.com/aks/ai-runtime/ray:.... - Secret handling: ✅ No secret env/volume changes.
- Network exposure: ✅ No new Services/Ingress/NetworkPolicies. Ports advertised on worker containers are
containerPortsonly; no cluster-scoped exposure.
Breaking Changes
- CRD upgrade behavior change on existing clusters (documented):
tau cluster installnow updates tau.azure.com CRDs before everyhelm upgrade. Existing CRs are retained (no--prune), and this is called out in both the plan output ("Tau CRDs: update from the selected chart before an existing-release upgrade; not rolled back by Helm") and the command long-form help. This is the intended fix for the CRD-vs-controller version drift and is safe under Kubernetes' additive-CRD semantics. - Head pod scheduling change: CPU head now tolerates
CriticalAddonsOnly:NoSchedule. This can cause the head to land on system nodes it previously would have been repelled from, which is desirable on AKS but worth noting for downstream operators who tightly constrain system-pool placement. Non-blocking; consistent with system-node affinity already present. - Ray worker container port surface change: Worker containers now declare the Serve container port. This changes the pod spec shape but has no runtime impact for RayJob (which doesn't create a Service) and enables per-worker Serve routing for RayService. Documented indirectly through the new tests and kind-smoke coverage.
Findings
Nothing blocking. Small observations only:
cli/internal/cli/cluster_install_crds.go:69— theerrors.As(err, &typeErr)branch handles theUpdating chart dependenciesprogress lines thathelm templateemits before the first YAML document; the testTestTauGridCRDManifestExcludesWorkloadsAndThirdPartyCRDsexercises this exact case. Nice defensive parse.cli/internal/cli/cluster_install_crds.go:40— afterrunner.Raw(... apply ...)returns,outis printed before the error check. This is correct (we wantconfigured/unchangedoutput visible even when the apply fails, so operators can see partial progress), but a brief comment noting the intent would help future readers. Non-blocking.cli/scripts/kind-smoke-e2e.sh:454— the legacy-workspace regression flow (patch CRD → create with legacy role → reinstall → assert UID preserved and role updated) is a strong migration test. The--dry-run=serverrole-update verification is a nice touch: it confirms the new schema accepts the alias without mutating the smoke resource.cli/internal/serve/ray_cluster.go:124— head port list is built by appending the head-only ports to the worker port slice; sinceportsis reassigned each pod iteration this is safe. If future changes ever hoistportsout of the loop, watch for aliasing.
Verdict
APPROVE. Delta is focused, well tested, and matches the documented behavior. All local checks (build, vet, unit tests for cli/internal/cli and cli/internal/serve) pass. The new CRD-upgrade path is guarded by server dry-run + Established wait + explicit chart-version parity, and the Ray renderer changes are covered by both unit fixtures and the kind smoke script.
|
Li Chen (@AaronLiChen) Added real Kueue/KubeRay RayService smoke coverage in d6c2c7f, with fixture memory sizing corrected in c560465: admission priority, two ready workers on port 9000, memory-backed shared memory, and an HTTP response. The fixture is CPU-adapted; GPU allocation and cross-host placement remain unit-tested, not GPU E2E claims. All CI is now green, including both Kind smoke jobs. |
Summary
Allow a Ray Serve application to use a multi-worker GPU profile instead of rejecting it as a single-Pod workload. Also align workspace creation and connection verification on the logical
researcherrole and grant the RayService access required for serving.--app-argsfor application builders and--workload-profile-snapshotfor explicitly offline client rendering. Keep connected workspace/profile checks mandatory for server dry-run and apply.researcher, accept the legacytau-researcher-v1alias, and update the generated CRDs, tests, examples, and documentation.Related issue
N/A.
Validation
Passed locally:
Go checks used
GOTOOLCHAIN=local,GOFLAGS=-mod=readonly, andAI_RUNTIME_E2E=0for tests. Missing cached dependencies were downloaded without changing module versions.The offline eight-worker render has one CPU head, eight single-GPU workers, and builder-owned application configuration. Regression tests cover rejected live use of snapshots, argument validation order, annotation preservation, and unchanged bindings across role aliases. Additional tests check RayService grants in both RBAC sources and reject each missing RayService permission for either role name. Twenty offline CLI scenarios also produced identical exit codes, stdout, and stderr before and after the simplification pass.
Not run for this PR: full repository
make check, full documentation site build, Kind integration tests, or live cluster/GPU tests.Compatibility and operational impact
--nodesand--gpusassert profile values; application replicas do not resize the GPU worker pool.--app-argsrequires an explicit application builder and rejects CLI replica/autoscaling overrides and legacy--args.--dry-run=client, rejects--context, and marks its output as snapshot-derived. It does not authorize deployment.role: researcher. Both role names bind the sametau-researcher-v1ClusterRole. Older CLIs that compare role names literally still need matching descriptor/workspace values.matchLabelKeys. RayService cluster upgrades can temporarily need capacity for both worker pools.Checklist