Skip to content

docs(spec): plan PD roles and cross-role atomic admission for ModelDeployment - #158

Closed
thxCode wants to merge 1 commit into
mainfrom
kvc-s7-spec
Closed

docs(spec): plan PD roles and cross-role atomic admission for ModelDeployment#158
thxCode wants to merge 1 commit into
mainfrom
kvc-s7-spec

Conversation

@thxCode

@thxCode thxCode commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind documentation
/area worker

What this PR does / why we need it:

Adds the design document for cross-role PD (prefill/decode) workloads and Kueue atomic
admission
. Spec only — 1 file, no implementation code, following the four docs(spec):
precedents on main (739d7b5a, 8c2cc0df, 674f5583, 57bfd123).

Prefill and decode only work if both halves run: a prefiller with no decoder produces KV blocks
nobody consumes, and a decoder with no prefiller has nothing to decode. The single-role
ModelDeployment spec creates one Kueue Workload per replica — correct for replicas that are
independently useful, and exactly wrong here: on a pool that fits half of them, Kueue admits half,
and which half is a race.

The spec lifts spec.roles from "exactly 1" to 1..10 and makes a deployment's Pods one Kueue
pod group
, so every role is admitted all-or-nothing. No new CRD. Three per-role fields:
kind (what the engine is told it is), acceleratorKey (which accelerator model inside the pool
the role wants), and nothing else.

Two constraints shape the whole design, both verified in this tree:

  • A Workload carries one queueName, so every role must sit in one ClusterQueue and
    roles[*].instanceType must be identical. The spec does not relax that and cannot.
  • Kueue assigns a ResourceFlavor per PodSet (flavorassigner.go: each PodSet forms its own
    assignment group by default), so inside one ClusterQueue two roles can still land on two
    accelerator models of the same manufacturer.

Across manufacturers stays impossible, structurally rather than for want of a feature: an
accelerated queue's quota is credits.gpustack.ai/<manufacturer> — one resource name per
manufacturer — and Kueue rejects a second resource group repeating a covered resource.

Status: Specified. The plan states one exception to its own ordering: the per-role feasibility
fix (F6/T10) ships first, on its own
, because it depends on neither the single-role spec nor
cross-model pools and repairs a defect reachable today.

Which issue(s) this PR links to:

None — this spec was not filed as an issue.

Special notes for your reviewer:

  • The spec is self-contained by design. Every external conclusion it uses is quoted with its
    evidence in the body, and external sources are cited by URL; it references no local file.
  • Verification honesty is explicit, not implied. Heterogeneous placement needs two accelerator
    models of one manufacturer in one pool, which the available hardware cannot provide, so it is
    verified in envtest against two fake ResourceFlavors — and the spec says why the fake is
    faithful at that layer
    : the property asserted ("Kueue assigned two different flavors to the two
    PodSets") is a Workload-status property, not a hardware one. The local-cluster case verifies
    atomic admission by starvation, asserting a negative (zero Pods of either role running).
    No throughput, latency or RDMA claim is made anywhere.
  • Two findings about existing code are recorded in Notes, and neither is in any task's Owns
    here:
    1. NodeDevicesAdmissionReconciler is not role-aware — that is F6/T10, shipping as its own PR.
    2. buildResourceGroups (node_queue.go:280) reads Kueue's MaxItems=16 as "16 flavors per
      resource group" and opens a second group with the same CoveredResources past 16. Kueue caps
      resource groups at 16 and flavors per group at 64, and validateResourceGroups
      declares its seenResources set outside the per-group loop, so a repeated covered resource is
      a field.Duplicate and the ClusterQueue update is rejected. A single-covered-resource queue
      can therefore only ever have one group; the split is itself the bug and the real ceiling
      is 64.
  • The deployed Kueue is 0.18.4 while the Go module is v0.17.1 (hack/deps.sh vs go.mod).
    Every runtime behaviour the spec relies on was read from 0.18.4, the version that runs; the
    skew is recorded as a standing caveat so the next reader does not take go.mod for the truth.

Does this PR introduce a user-facing change?

NONE

…ployment

Prefill and decode only work if both halves run: a prefiller with no
decoder produces KV blocks nobody consumes. The single-role
ModelDeployment spec creates one Kueue Workload per replica, which is
right for replicas that are independently useful and exactly wrong here —
on a pool that fits half of them, Kueue admits half and which half is a
race.

This spec lifts spec.roles from "exactly 1" to 1..10 and makes a
deployment's Pods one Kueue pod group, so its roles are admitted
all-or-nothing. No new CRD. Three per-role fields: kind (what the engine
is told it is), acceleratorKey (which accelerator model inside the pool
the role wants), and nothing else.

Two constraints shape it, both verified in this tree. A Workload carries
one queueName, so every role must sit in one ClusterQueue and
roles[*].instanceType must be identical — this spec does not relax that
and cannot. But Kueue assigns a ResourceFlavor per PodSet, so inside one
ClusterQueue two roles can still land on two accelerator models of the
same manufacturer. Across manufacturers stays impossible: a queue's
accelerator quota is credits.gpustack.ai/<manufacturer>, one resource name
per manufacturer, and one resource group covers one set of resources.

Status: Specified. Implementation follows the single-role spec, with one
exception the plan states: the per-role feasibility fix (F6/T10) depends
on neither that spec nor cross-model pools and ships first, on its own,
because it repairs a defect reachable today.

Signed-off-by: thxCode <thxcode0824@gmail.com>
@gpustack-codereview

Copy link
Copy Markdown

OpenCodeReview: Review skipped: no items were selected.

@thxCode

thxCode commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Closing this: opening it was premature, and the repository's own history says so more clearly than
the precedent I opened it on.

Three of the four docs(spec): commits I cited did land before their implementation — but they
landed as Status: Planned, and their work started immediately after. Across all 33 specs on
main, the statuses ever used on first landing are Shipped (21), Building (8), Planned (3)
and Built (1). Specified — what this spec carries — has never appeared on main; it is the
drafting state.

Two things make this case unlike those three precedents:

  • This spec's implementation is blocked on the single-role ModelDeployment spec, which has not
    started. So the gap between landing and building is open-ended rather than days.
  • It specifies 10 features and one is built. The design has not yet been validated by
    implementation, so an amendment would land on main.

The spec is unchanged on the kvc-s7-spec branch and will land when the work starts — as
Building, or with the implementation.

#159 is unaffected and stays open. It repairs a defect reachable on main today (a node
carrying two accelerator models has its other model's cards counted toward a demand its flavor does
not cover), carries its own evidence, and does not depend on this spec being merged. Its
description has been updated so it no longer refers to this PR.

@thxCode thxCode closed this Sep 2, 2026

Copilot AI 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.

🟡 Changes recommended

Critical and moderate correctness gaps remain in Service isolation, role composition, validation, status, naming, and testing.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a design specification for multi-role ModelDeployment workloads with atomic Kueue admission.

Changes:

  • Defines role APIs, validation, placement, Services, and status.
  • Plans per-role flavor selection and feasibility checks.
  • Outlines implementation and testing.
File summaries
File Description
specs/2026-09-02-model-deployment-pd-atomic-admission.md Designs cross-role atomic admission and implementation milestones.
Review details

Suppressed comments (2)

specs/2026-09-02-model-deployment-pd-atomic-admission.md:16

  • The summary says three fields but names only two; the implementation plan confirms two additions to ModelDeploymentRole (kind and acceleratorKey) plus separate status fields. State that this adds two per-role spec fields so the public API summary is unambiguous.
length 1; this spec lifts that bound to 1..10 and adds three per-role fields — `kind` (what the
engine is told it is), `acceleratorKey` (which accelerator model inside the pool this role wants) and
nothing else. The Pods of a deployment become one **Kueue pod group**, one PodSet per role, and the

specs/2026-09-02-model-deployment-pd-atomic-admission.md:1006

  • This API example contradicts F8 and T1: ModelDeploymentRoleStatus is required to gain both kind and assignedFlavor, but the shown struct adds only AssignedFlavor. Add Kind to the example and reconcile the summary's per-role field count so T1 has one unambiguous API shape.
type ModelDeploymentRoleStatus struct {
  • Files reviewed: 1/1 changed files
  • Comments generated: 9
  • Review effort level: Balanced

Comment on lines +1115 to +1117
Acceptance: in addition to the deployment-wide `Service`, one `ClusterIP` Service per role
selecting that role's Pods through the `worker.gpustack.ai/role` label, owned by the deployment,
named `<deployment>-<role>`. `status.endpoint` stays the deployment-wide one. A single-role
Comment on lines +138 to +140
The single-role spec's plain round-robin `Service` is unchanged; a P/D deployment gets one
`Service` per role plus the deployment-wide one, and nothing steers between them. This boundary
exists to agree with the one below: a spec that cannot verify the transport must not specify it.
Comment on lines +386 to +388
**A group that is short of its total is reported.** `QuotaReserved` goes `False` with reason
`PodGroupIncomplete`, and the message names how many Pods exist against how many the group declares.
The predicate is observed: the number of the deployment's own Pods that exist, against Σ `replicas`.
Comment on lines +446 to +450
So the webhook resolves the role's `instanceType` to its ClusterQueue, reads the pool's live
`ResourceFlavor`s, and rejects a key none of them offers, naming the offered set. A pool that has no
flavors yet (a fresh cluster) is **not** a rejection — the key may become valid in a minute — it is
accepted, and the mismatch then shows up as F6's `Retry`, which is the transient-shortage path
already.
Comment on lines +508 to +510
`kind: server` alongside any other kind is refused: "one plain server plus a prefiller" is not a
shape anything consumes, and accepting it would mean rendering a connector configuration whose
meaning is undefined.
Comment on lines +558 to +562
- A demand is carried **with the PodSet it came from**, and two PodSets' demands merge only when they
were assigned the **same flavor** (in addition to today's family/units/profile agreement) — so
co-located roles still share a budget and disjoint roles do not.
- A card is carried **with the flavor(s) whose node selector and accelerator key it satisfies**, and a
demand is fitted only against cards its own PodSet's flavor covers.
Comment on lines +1115 to +1117
Acceptance: in addition to the deployment-wide `Service`, one `ClusterIP` Service per role
selecting that role's Pods through the `worker.gpustack.ai/role` label, owned by the deployment,
named `<deployment>-<role>`. `status.endpoint` stays the deployment-wide one. A single-role
Comment on lines +1315 to +1319
- **envtest, two fake flavors of two models in one ClusterQueue**, a two-role deployment naming both:
the Workload's `podSetAssignments` carries **two different flavors**. This is where G2 is verified;
the hardware cannot show it.
- **envtest, the incomplete group** (T9): the group short of its total ⇒ **no Workload exists** and
the deployment reports `PodGroupIncomplete`; completing the group produces the Workload and clears
Comment on lines +116 to +125
- **Cross-manufacturer heterogeneous P/D.** Prefill on NVIDIA and decode on Ascend is not
expressible, and the reason is structural: `buildResourceGroups`
(`pkg/worker/controllers/worker/node_queue.go:261`) sets an accelerated queue's `CoveredResources`
to the single resource `credits.gpustack.ai/<manufacturer>`, derived from the pool's one
`manufacturer` note. A ClusterQueue resource group covers one set of resources, and Kueue's own
ClusterQueue webhook rejects a second group repeating a covered resource
(`validateResourceGroups`). Two manufacturers' credits cannot be one queue's quota, and one
`queueName` per Workload does the rest.
- Same-manufacturer, **cross-model** heterogeneous P/D *is* in scope (G2). The two are not the
same restriction and must not be stated as one.
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