Skip to content

feat(templates): declarative K8S compute configs for 3 pilot templates - #940

Open
Aydin-ab wants to merge 3 commits into
mainfrom
add-azure-declarative-compute-config
Open

Aydin-ab wants to merge 3 commits into
mainfrom
add-azure-declarative-compute-config

Conversation

@Aydin-ab

@Aydin-ab Aydin-ab commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Adds an optional K8S: compute-config key pointing at a declarative k8s.yamlrequired_resources shapes resolved server-side into free pods, no registered instance types, one config covering AKS/EKS/GKE. Three pilots span the shape space: workspace-intro (single node), getting-started (multi-node CPU), pytorch-fsdp (multi-node GPU, 2×T4 via required_labels: {ray.io/accelerator-type: T4} — Azure GPU pools have no L4/A10G). The validator accepts the new key and enforces the declarative contract: no instance_type, no auto_select_worker_config (incompatible with declarative configs), and GPU counts paired with the accelerator-type label (launch validation reads the label, not a required_resources.accelerator key).

Groundwork for the Azure/K8s template path: rayapp build already carries the key into bundles as-is; the product launch path selecting + honoring it lands separately (today it force-selects AWS and flattens on K8s clouds), so the key is inert in prod until then.

Testing

  • pre-commit run --all-files green (BUILD.yaml validator gate included).
  • rayapp build: all 56 bundles build; the three pilots' ray-app.json carry the K8S config with required_resources + required_labels preserved verbatim.
  • All three files parse through anyscale.compute_config.models.ComputeConfig (instance_typecustom).
  • Shapes checked against https://docs.anyscale.com/configuration/compute/declarative and the backend's check_gpu_accelerator_consistency (GPU count must pair with the ray.io/accelerator-type label; auto-select unsupported with declarative).
  • Validator negative cases exercised: instance_type, auto_select_worker_config, GPU without label, accelerator inside required_resources, empty required_resources, non-dict nodes, cross-dir K8S path, wrong filename — each fails with a targeted error.

https://claude.ai/code/session_013BsS5dSk6s6xsLx2FbBy7g

The AWS/GCP compute keys name VM instance types, which K8s-stack
clouds (AKS/EKS/GKE) cannot honor — the backend currently flattens
them to a head-only CPU pod at launch. A declarative k8s.yaml
(required_resources) instead resolves server-side into free pods,
needing no registered instance types, and one config covers every
K8s stack — hence a single K8S key rather than per-provider ones.

Pilots span the shape space: single node + auto-select
(workspace-intro), multi-node CPU (getting-started), multi-node GPU
(pytorch-fsdp, 2x T4 — chosen because Azure GPU pools have no
L4/A10G). The validator keeps k8s.yaml declarative (instance_type
is an error) so the file class cannot drift back into per-cluster
registration dependencies.

Claude-Session: https://claude.ai/code/session_013BsS5dSk6s6xsLx2FbBy7g
Signed-off-by: Aydin Abiar <aydin@anyscale.com>
@Aydin-ab
Aydin-ab requested a review from a team as a code owner August 8, 2026 01:22
A non-dict node (e.g. a bare string in worker_nodes) crashed the hook
with a traceback before collected errors printed — pydantic already
reports those, so skip them here. And required_resources: {} slipped
past the is-None guard while the SDK rejects it at deploy time; treat
empty as missing.

Claude-Session: https://claude.ai/code/session_013BsS5dSk6s6xsLx2FbBy7g
Signed-off-by: Aydin Abiar <aydin@anyscale.com>
Two authoring errors vs the declarative contract
(docs.anyscale.com/configuration/compute/declarative + product
compute_templates.py check_gpu_accelerator_consistency):
auto_select_worker_config is not compatible with declarative configs,
and the GPU type must ride in required_labels
(ray.io/accelerator-type) — the launch-time GPU validation never
reads required_resources.accelerator, so the previous form would
have been rejected at launch.

basic-single-node becomes an explicit single cpu_worker group, heads
slim to coordinator sizing (4CPU/8Gi, they are CPU: 0), the FSDP GPU
workers keep the AWS-proven g4dn.xlarge shape (4CPU/16Gi/1xT4). The
hook now enforces all three rules so the next k8s.yaml can't repeat
this.

Claude-Session: https://claude.ai/code/session_013BsS5dSk6s6xsLx2FbBy7g
Signed-off-by: Aydin Abiar <aydin@anyscale.com>
@@ -0,0 +1,12 @@
head_node:

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.

can we rename them to azure.yaml to match the convention of the other templates?

elliot-barn added a commit to ray-project/rayci that referenced this pull request Sep 17, 2026
A K8s-stack cloud (AKS/EKS/GKE) cannot honor a named instance type:
those are per-cluster registrations, while `required_resources`
resolves server-side into a free pod. So whatever a template puts under
the K8S key is packaged declaratively:

  K8S: configs/<t>/k8s.yaml   the authored config, put through the same
                              translation -- a no-op when it is already
                              declarative, and it converts any
                              instance_type the file still names
  K8S: auto                   derived from the template's AWS config
  (no K8S key)                no K8S config, exactly as before

AWS and GCP are packaged as they always have been. Deriving is opt-in
per template, and a template that asks for one and cannot have it fails
the build rather than quietly shipping without.

What "declarative" means here
-----------------------------
Nothing in the schema flags it. ComputeNodeType carries instance_type
and required_resources both as optional -- "Optional when using free
pod shapes with required_resources" -- and which one is set is the
whole difference. So the decision is per node, on what that node
carries, never on a filename or a provider key, and a node that is
already declarative passes through untouched. The docs make this a
rule: "Each node group must use either predefined instances or
declarative syntax, not both"
(docs.anyscale.com/configuration/compute/declarative), so a node
setting both is reported rather than silently half-honored.

Translation
-----------
- instance_type -> required_resources {CPU, memory, GPU} from a
  hand-maintained shape table; an unlisted type is reported, not
  guessed, since a wrong shape is a pod that never schedules.
- GPU instances also get required_labels ray.io/accelerator-type; the
  launch-time GPU validation reads the label, never
  required_resources.accelerator. Values stay inside the documented
  set, which a test enforces.
- An unschedulable head (CPU: 0) only coordinates, so it gets
  coordinator sizing rather than the whole machine.
- auto_select_worker_config has no declarative form, and it is the
  most common template shape (21 templates share one auto-select
  config). One group shaped like the head's machine stands in, the
  same substitution the hand-written single-node config makes. This is
  the only invented shape here.

Memory is emitted as integer bytes: the published bundle is parsed
straight into the backend's PhysicalResources, whose memory is an int.

Test
----
The rules were derived from the three hand-written pilots in
anyscale/templates#940 and reproduce them. Building that branch with
`K8S: auto` in place of each authored path gives a config
byte-identical to the authored getting-started/k8s.yaml and
basic-single-node/k8s.yaml, and identical to pytorch-fsdp/k8s.yaml
except the worker group name, which is carried over from the VM config
rather than renamed. Those three are golden tests here.

Also verified end to end on anyscale/templates: an authored k8s.yaml
round-trips unchanged with AWS and GCP byte-identical, `K8S: auto`
derives, and a template with no K8S key is untouched.

go test ./rayapp/ passes, coverage 87.0% (gate 80%); gofmt and go vet
clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ray CI Test <rayci@ray.io>
elliot-barn added a commit to ray-project/rayci that referenced this pull request Sep 18, 2026
The API takes `required_resources.memory` as an integer number of bytes,
but every user-facing surface advertises Kubernetes quantity strings
("8Gi"). The SDK reconciles those in PhysicalResources.to_dict(for_api=
True) -> _parse_memory_string; the published bundle never touches the
SDK, since rayapp copies required_resources verbatim into ray-app.json
and the console clone path parses it straight into the backend's
PhysicalResources, whose `memory` is an Optional[int]. So "8Gi" passes
`anyscale compute-config create -f` and then 422s at launch:

  body.config.head_node_type.required_resources.memory:
    value is not a valid integer

Convert in the Go mirror too, so the two conversion paths agree and a
template can be authored the way the docs describe.

Parse with k8s.io/apimachinery's resource.ParseQuantity -- the same
parser Kubernetes uses, and the one the SDK reaches through
kubernetes.utils.quantity -- rather than hand-rolling the grammar, so
the accepted syntax cannot drift from what the docs describe. Pinned to
apimachinery v0.34.0, which needs only go 1.24; v0.37 would drag the go
directive to 1.26 and bump x/net.

Three rules on top of the parser, each because the value has to land in
an integer bytes field:

- Whole bytes only. Kubernetes tolerates a fraction ("100m" is 100
  *milli*bytes) but its own docs say "this isn't useful to specify
  since you must always assign whole numbers of bytes", and a fraction
  could not survive PhysicalResources.memory anyway. This also removes
  the one place the two parsers disagree: ParseQuantity().Value()
  rounds up where the SDK's int(Decimal) truncates, and that only
  differs for values we now reject.
- No negatives. Nothing downstream rejects them -- the backend only
  range-checks int64 -- so a negative would reach launch as a nonsense
  pod request. YAML parses `memory: -1` as an int, so the integer path
  needs this too.
- Must contain a digit. ParseQuantity reads a bare suffix ("Gi", "k",
  ".") as zero, so a typo would silently become no memory at all. The
  SDK rejects those.

TestParseMemoryBytesMatchesSDK runs every golden through the SDK
function this mirrors and compares, so the two cannot drift unnoticed;
it skips where python3 or the anyscale package is unavailable.

No effect on VM configs: this fires only on required_resources.memory,
which instance-type configs (every aws.yaml/gce.yaml in
anyscale/templates today) never set. Building anyscale/templates#940
with this binary gives byte-identical AWS and GCP configs and converts
only the K8S memory values.

Test: go test ./rayapp/ passes, coverage 87.5% (gate 80%); gofmt and go
vet clean. golangci-lint panics on this package on a clean tree too
(toolchain mismatch: built with go1.25, sources need go1.26).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ray CI Test <rayci@ray.io>
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.

2 participants