Skip to content

Commit 64dac67

Browse files
authored
tests: composed orchestrator integration suite against both official charts (#129)
* tests: composed orchestrator integration suite against both official charts Add the orchestrator phase to the integration test launcher. run.sh installs BOTH official Helm charts (snapshot-agent on CHART_AGENT_PORT, timeslice-orchestrator with snapshotAgentPort matching) and drives real scenario workloads through the orchestrator gRPC API. Files added: - orchestrator/orchestrator_test.go: TestOrchestrator with SingleRLJob (QueuedRLJobs omitted, blocked by orchestrator cold-start IDLE bug) - orchestrator/harness.go: ComposedHarness attaching to chart-deployed components, labeling TEST_NODE with group labels, pre-cleaning leaked pods from prior runs Files modified: - run.sh: --orch-image flag, --phase orchestrator|all, orchestrator chart install/cleanup, ORCH_CHART_DEPLOYED env var - runner.yaml: node update/patch RBAC for group labeling, ResourceClaims create/delete/get/list for DRA scenarios - README.md: orchestrator phase documentation - deploy/timesliceorchestrator: snapshotAgentPort value + deployment args pass-through - harness/harness.go: WaitPodReadyByLabel accepts empty node Signed-off-by: Aishu Krishnamurthy <aishu.krishnamurthy@gmail.com> Signed-off-by: Aishu Kamal <aishuk@google.com> * tests: 2-node topology, PyTorch default template, QueuedRLJobs enabled Address review: each group gets its own GPU node (canonical deployment), default pod template is PyTorch (fast, sufficient for lock/preemption), and QueuedRLJobs is enabled (acquire-before-deploy model does not hit #137). Signed-off-by: Aishu Kamal <aishuk@google.com> * tests: update README for 2-node orchestrator topology * tests: list 'both' phase in run.sh usage string --------- Signed-off-by: Aishu Kamal <aishuk@google.com>
1 parent 3bad43c commit 64dac67

10 files changed

Lines changed: 503 additions & 75 deletions

File tree

deploy/timesliceorchestrator/templates/deployment.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ spec:
2020
- name: {{ .Chart.Name }}
2121
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
2222
imagePullPolicy: {{ .Values.image.pullPolicy }}
23+
args:
24+
- "--snapshot-agent-port={{ .Values.snapshotAgentPort }}"
2325
ports:
2426
- name: grpc
2527
containerPort: 50051

deploy/timesliceorchestrator/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ service:
99
type: ClusterIP
1010
port: 50051
1111

12+
snapshotAgentPort: 9001
13+
1214
resources: {}
1315
# limits:
1416
# cpu: 100m

tests/integration/README.md

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
# Integration Tests
22

3-
End-to-end tests for snapshot-agent backends on real GPU hardware, in k8s and standalone modes, exercising the Helm chart and Makefile deployment paths respectively.
3+
End-to-end tests for the time-slicing stack on real GPU hardware, exercising the official Helm chart deployment paths.
44

5-
The test suite is written in Go and runs inside the cluster: `run.sh` deploys a test-runner pod, copies the repo source into it, and executes `go test` there. The Go harness deploys the snapshot-agent and inference engine pods itself — one engine at a time, so a single free GPU is enough.
5+
The test suite is written in Go and runs inside the cluster: `run.sh` deploys a test-runner pod, copies the repo source into it, and executes `go test` there.
66

7-
All snapshot/restore calls go through the **Python client** (`timeslice.snapshot_agent`, invoked via `agentctl.py`), so the entire client layer is covered.
7+
## Suites
88

9-
- `run.sh` — launcher (build image, install chart fixture, deploy runner, copy source, build `make standalone`, install the Python client, `go test`, cleanup)
10-
- `runner.yaml` — test-runner pod + RBAC
11-
- `harness/` — shared framework: in-cluster client, node selection, pod lifecycle, exec/HTTP/VRAM helpers
12-
- `snapshot-agent/` — the agent suite: `standalone_test.go` / `k8s_test.go`, plus the agent specifics (`harness.go` agent deployment, `engines.go` engine specs, `agentctl.py` — a thin CLI over the Python client that builds `BackendConfig` protos from primitive flags)
9+
### snapshot-agent (phases: standalone, k8s)
1310

14-
**How the standalone mode for snapshot-agent works:** since the test suite runs inside a GKE cluster, standalone mode is simulated by deploying a privileged pod with `hostPID` and `hostNetwork` on the test node. The `make standalone` artifacts are built in the test runner and copied into this pod, which then runs the agent binary with the same GPU and PID namespace access as a host process. Long-term, standalone tests will run on an actual GPU VM.
11+
Tests snapshot-agent backends in standalone and k8s modes. The Go harness deploys the snapshot-agent and inference engine pods itself -- one engine at a time, so a single free GPU is enough. All snapshot/restore calls go through the **Python client** (`timeslice.snapshot_agent`, invoked via `agentctl.py`), so the entire client layer is covered.
12+
13+
**How the standalone mode works:** since the test suite runs inside a GKE cluster, standalone mode is simulated by deploying a privileged pod with `hostPID` and `hostNetwork` on the test node. The `make standalone` artifacts are built in the test runner and copied into this pod, which then runs the agent binary with the same GPU and PID namespace access as a host process.
14+
15+
### orchestrator (phase: orchestrator)
16+
17+
Composed orchestrator integration suite. Installs BOTH official Helm charts (snapshot-agent + timeslice-orchestrator) and drives real orchestrator scenarios through the gRPC API. The orchestrator chart is configured with `snapshotAgentPort` matching `CHART_AGENT_PORT` so it commands the suite's own agent.
18+
19+
Uses a 2-node topology: `TEST_NODE_SAMPLERS` for the samplers group, `TEST_NODE_TRAINERS` for the trainers group (one GPU per node, separate nodes). `exclusiveLabel` temporarily removes group labels from other nodes so pods land only on the designated test nodes.
20+
21+
## Layout
22+
23+
- `run.sh` -- launcher (build images, install chart fixtures, deploy runner, copy source, build `make standalone`, install the Python client, `go test`, cleanup)
24+
- `runner.yaml` -- test-runner pod + RBAC
25+
- `harness/` -- shared framework: in-cluster client, node selection, pod lifecycle, exec/HTTP/VRAM helpers
26+
- `snapshot-agent/` -- the agent suite: `standalone_test.go` / `k8s_test.go`, plus the agent specifics (`harness.go` agent deployment, `engines.go` engine specs, `agentctl.py`)
27+
- `orchestrator/` -- the orchestrator suite: `orchestrator_test.go` / `harness.go`
28+
- `orchestrator/scenarios/` -- scenario drivers shared by both the simulate tier (unit tests) and the composed suite
29+
- `orchestrator/simulate/` -- fakes tier: in-process orchestrator with fake K8s, runs on every PR
1530

1631
## Adding a test
1732

18-
Add a `t.Run(...)` inside the engine group that provides the pods it needs, using the harness helpers:
33+
**snapshot-agent:** Add a `t.Run(...)` inside the engine group that provides the pods it needs, using the harness helpers:
1934

2035
```go
2136
h.WithEngine(t, VLLM, func(t *testing.T, e *Engine) {
@@ -31,75 +46,92 @@ h.WithEngine(t, VLLM, func(t *testing.T, e *Engine) {
3146

3247
A new engine is an `EngineSpec` in `snapshot-agent/engines.go`.
3348

49+
**orchestrator:** Add a scenario to `orchestrator/scenarios/` and call it from the `TestOrchestrator` function in `orchestrator/orchestrator_test.go`.
50+
3451
## Prerequisites
3552

3653
- A GKE cluster with at least 1 free GPU
3754
- `gcloud` and `kubectl` on the machine running the tests
3855
(Go and everything else run inside the cluster)
39-
- For the k8s phase: a snapshot-agent image built from the official
40-
Dockerfile (`--build` does this for you; `cloudbuild-image.yaml` defaults
41-
to `docker/snapshot-agent/Dockerfile`). The standalone phase needs no
42-
image — it builds the agent from source in the test runner.
56+
- For the k8s/orchestrator phases: images built from the official
57+
Dockerfiles (`--build` does this for you). The standalone phase needs no
58+
image -- it builds the agent from source in the test runner.
4359
- Cloud Build API enabled (`gcloud services enable cloudbuild.googleapis.com`)
4460
and permission to push to the project's registry; GKE nodes in the same
4561
project can pull from `gcr.io/<project>` by default.
4662

4763
## Testing your changes
4864

49-
Everything runs from your working directory uncommitted changes included
65+
Everything runs from your working directory -- uncommitted changes included --
5066
so no commit or merge is needed at any layer:
5167

5268
```bash
69+
# snapshot-agent phases (standalone + k8s):
5370
TEST_NODE=<gpu-node> ./tests/integration/run.sh \
5471
--build --project <gcp-project>
72+
73+
# orchestrator phase only (2 GPU nodes, one per group):
74+
TEST_NODE_SAMPLERS=<gpu-node-1> TEST_NODE_TRAINERS=<gpu-node-2> \
75+
./tests/integration/run.sh \
76+
--build --project <gcp-project> --phase orchestrator
77+
78+
# everything (standalone + k8s on TEST_NODE, orchestrator on the 2-node topology):
79+
TEST_NODE=<gpu-node> TEST_NODE_SAMPLERS=<gpu-node-1> TEST_NODE_TRAINERS=<gpu-node-2> \
80+
./tests/integration/run.sh \
81+
--build --project <gcp-project> --phase all
5582
```
5683

57-
`--build` has Cloud Build produce the agent image from the working directory
84+
`--build` has Cloud Build produce the images from the working directory
5885
(tagged `integ-<commit>` so repeated runs don't collide with node image
59-
caches) and runs both phases against it. `run.sh` then copies the local
60-
workspace into the cluster, so the standalone phase's `make standalone`
61-
build, the Helm chart (installed from local `deploy/`), the Python client,
62-
and the test code all come from the workspace too.
86+
caches).
6387

64-
Alternative (pre-built image — any registry the cluster can pull from):
88+
Alternative (pre-built images -- any registry the cluster can pull from):
6589

6690
```bash
67-
gcloud builds submit --config=cloudbuild-image.yaml \
68-
--substitutions=_IMAGE=gcr.io/<project>/snapshot-agent:dev .
69-
70-
TEST_NODE=<gpu-node> ./tests/integration/run.sh \
71-
--agent-image gcr.io/<project>/snapshot-agent:dev
91+
TEST_NODE=<gpu-node> TEST_NODE_SAMPLERS=<gpu-node-1> TEST_NODE_TRAINERS=<gpu-node-2> \
92+
./tests/integration/run.sh \
93+
--agent-image gcr.io/<project>/snapshot-agent:dev \
94+
--orch-image gcr.io/<project>/timesliceorchestrator:dev \
95+
--phase all
7296
```
7397

7498
## Options
7599

76100
```text
77-
--agent-image IMAGE Snapshot-agent image the k8s phase installs via the
78-
official chart (required for k8s/both unless --build)
79-
--build Build the agent image from the working directory via
80-
Cloud Build (requires --project); explicit
81-
--agent-image overrides
101+
--agent-image IMAGE Snapshot-agent image (required for k8s/orchestrator
102+
unless --build)
103+
--orch-image IMAGE Orchestrator image (required for orchestrator unless
104+
--build)
105+
--build Build images from the working directory via Cloud Build
106+
(requires --project); explicit --agent-image /
107+
--orch-image overrides
82108
--project PROJECT GCP project (required with --build for image pushes;
83109
also used by gcloud get-credentials with --cluster)
84110
--cluster CLUSTER GKE cluster name (optional; omit to use current
85111
kubectl context)
86112
--zone ZONE GKE cluster zone (optional)
87113
--model MODEL Model to load (default: Qwen/Qwen2.5-0.5B)
88-
--phase PHASE "standalone", "k8s", or "both" (default)
89-
--skip-cleanup Leave the test-runner pod and chart fixture running
114+
--phase PHASE "standalone", "k8s", "orchestrator", "both" (default,
115+
= standalone+k8s), or "all" (= standalone+k8s+orch)
116+
--skip-cleanup Leave the test-runner pod and chart fixtures running
90117
for debugging
91118
```
92119

93120
Environment:
94121

95-
- `TEST_NODE=<node-name>` — required for the k8s phase (the chart is pinned
96-
to this node); for the standalone phase it pins the suite instead of the
97-
default pick (first node with a free GPU by requests). Use the pin when
98-
the cluster runs workloads that occupy GPUs without requesting them
99-
(time-slicing experiments), which the default pick cannot see.
100-
- `CHART_AGENT_PORT=<port>` — port for the chart-deployed agent (default
101-
9001), so the suite can coexist with an unrelated agent on the default
102-
port (the chart runs on hostNetwork).
122+
- `TEST_NODE=<node-name>` -- required for the standalone and k8s phases
123+
(charts are pinned to this node); for the standalone phase it pins the
124+
suite instead of the default pick (first node with a free GPU). Use the
125+
pin when the cluster runs workloads that occupy GPUs without requesting
126+
them (time-slicing experiments), which the default pick cannot see.
127+
- `TEST_NODE_SAMPLERS=<node-name>` -- required for the orchestrator phase.
128+
GPU node for the samplers group (must be different from `TEST_NODE_TRAINERS`).
129+
- `TEST_NODE_TRAINERS=<node-name>` -- required for the orchestrator phase.
130+
GPU node for the trainers group (must be different from `TEST_NODE_SAMPLERS`).
131+
- `CHART_AGENT_PORT=<port>` -- port for the chart-deployed agent (default
132+
9002), so the suite can coexist with an unrelated agent on the default
133+
port (the chart runs on hostNetwork). The orchestrator chart is
134+
configured with the same port via `snapshotAgentPort`.
103135

104136
## Exit code
105137

tests/integration/harness/harness.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,18 @@ func (c *Cluster) WaitPodReady(t *testing.T, name string, timeout time.Duration)
152152
}
153153

154154
// WaitPodReadyByLabel waits for a Ready pod matching labelSelector on the
155-
// given node in the given namespace and returns its IP. It attaches to pods
156-
// deployed by something other than the suite (e.g. a Helm chart DaemonSet).
155+
// given node in the given namespace and returns its IP. If node is empty,
156+
// any node is accepted. It attaches to pods deployed by something other
157+
// than the suite (e.g. a Helm chart DaemonSet or Deployment).
157158
func (c *Cluster) WaitPodReadyByLabel(t *testing.T, namespace, labelSelector, node string, timeout time.Duration) string {
158159
t.Helper()
159160
deadline := time.Now().Add(timeout)
160161
for time.Now().Before(deadline) {
161-
pods, err := c.Client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
162-
LabelSelector: labelSelector,
163-
FieldSelector: "spec.nodeName=" + node,
164-
})
162+
opts := metav1.ListOptions{LabelSelector: labelSelector}
163+
if node != "" {
164+
opts.FieldSelector = "spec.nodeName=" + node
165+
}
166+
pods, err := c.Client.CoreV1().Pods(namespace).List(context.Background(), opts)
165167
if err == nil {
166168
for i := range pods.Items {
167169
pod := &pods.Items[i]

0 commit comments

Comments
 (0)