8000 feat(rod): show per-user dominant resource allocation for selected queue by janzak-dev · Pull Request #1587 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content
8000

feat(rod): show per-user dominant resource allocation for selected queue - #1587

Closed
janzak-dev wants to merge 4 commits into
mainfrom
feat/rod-user-resource-breakdown
Closed

janzak-dev wants to merge 4 commits into
mainfrom
feat/rod-user-resource-breakdown

Conversation

@janzak-dev
Copy link
Copy Markdown
Collaborator

Summary

The Resource Observability Dashboard (/resources) now shows how much of the dominant resource each user holds in the selected queue. The dominant resource is the first resource picked in the Smart Resource Finder's "I need" picker. A stacked bar with one chip per user sits above the jobs table, so you can see who to ping without paging through jobs.

Motivation & Context

Part of #1515 (the "Who holds the used GPUs?" bar in the mockups). The jobs table is paginated, so answering "who is using this queue?" meant clicking through every page and adding up the Resources column by hand. The breakdown is computed server-side across all active work units of the queue, so it doesn't depend on pagination. It is not hardcoded to GPUs: the dominant resource can be a specific accelerator model (nvidia.com/gpu: nvidia-tesla-h100), a generic parent (nvidia.com/gpu, which sums every matching model), or cpu / memory.

Key Changes

  • API (additive): new UserResourceAllocation { user, resource_type, running_units, pending_units } and GetRunningAndPendingJobsResponse.user_allocations = 4. All resource types are returned and the UI picks what to show.
  • Storage (Spanner + Postgres): new WorkUnitSnapshotRepository with FetchActiveWorkUnitsByQueues and AggregateActiveResourcesByAuthor, plus WorkUnitStore.WithReadOnlyWorkUnitSnapshot, so the jobs page and the aggregation are read from one read-only snapshot (Spanner ReadOnlyTransaction, Postgres REPEATABLE READ READ ONLY). Rows are grouped by Experiments.Creator, canonical resource type and running vs pending. The existing FetchActiveWorkUnitsByQueues now delegates to the snapshot. Shared aggregation lives in internal/storage/data; MockStorage is updated.
  • Resource canonicalization: internal/common/resourceaccumulator/canonical.go maps symbolic GPU types, GKE accelerator labels and TPU models to the same compound names Kueue queues use (nvidia.com/gpu: <model>, google.com/tpu: <model>) and normalizes memory to GiB. FormatCompoundAccelerator and BytesPerGiB move here from rod_data_provider, removing a duplicate.
  • rod_data_provider: GetRunningAndPendingJobs fills user_allocations in the same snapshot as the jobs page, sorted by total units. A generic nvidia.com/gpu request is attributed to the queue's GPU model when the queue has exactly one GPU flavor.
  • UI: new standalone QueueUserAllocationBar (signals, OnPush, @if/@for, harness tests) rendered inside the jobs card above the table. It shows a stacked bar plus display-only chips with running and pending units and the unit label (GPUs / chips / cores / GiB), and filters with the existing matchesResourceFilter. With no resource picked it shows a hint; when nothing matches it shows an empty message. ResourceDashboard passes dominantResource to QueueJobsTable.
  • Docs: docs/flows/metrics_and_observability.md section 5.

Assumptions

  • Running = RUNNING / STARTING (admitted by Kueue, holding quota); pending = SCHEDULING / INITIALIZING. This follows the reconciler's state mapping and the status-history "admitted" states. If wrong, units shift between the running and pending labels; per-user totals are unaffected.
  • Units are requested amounts from each work unit's executable, not measured utilization; utilization in chips is out of scope. If expectations differ, the bar can disagree with actual usage (for example, a pending job holds nothing yet but still appears as pending).
  • The aggregation reads the executable of every active work unit in the queue on each jobs request, with no cap. That's acceptable for current queue sizes. On very large queues (thousands of active jobs) it adds latency to GetRunningAndPendingJobs; a follow-up could cap it or precompute it.
  • The symbolic GPU type to GKE accelerator mapping in canonical.go mirrors the table used when launching on GKE. If that table changes without this one, those requests show up under a different model name, though they still sum under the generic nvidia.com/gpu parent.
  • A generic GPU request is attributed to the queue's GPU model only when the queue has exactly one GPU flavor; otherwise it stays nvidia.com/gpu. If wrong, a specific-model filter can miss generic requests in single-flavor queues (the parent filter always includes them).
  • An empty creator is shown as unknown in the UI.
  • Shipped as one PR although it exceeds the 400 / 1,000-line sizing gate (about 1,065 production / 1,118 test lines), at the author's request. The shared WorkUnitStore interface forces both backends into the same change. A vertical split was evaluated (API+storage → UI → snapshot/flavor resolution) and can still be applied if reviewers prefer.
  • Out of scope: click-to-filter on chips, sort/filter (Filter and sort queue jobs before pagination #1516), per-GPU or borrowed quota, utilization.

Verification & Testing

  • Commands executed: bazel test on //internal/common/resourceaccumulator/..., //internal/storage/... (Spanner emulator and Postgres), //cmd/rod_data_provider/... and //cmd/ui:ui_unit_tests; ./.githooks/pre-commit (formatters, linters, run_affected_tests.py: 300/300 affected tests pass); npx playwright test screenshot_tests/resource_dashboard (existing /resources baselines pass).
  • Storage tests (both backends, shared fixture): two users, running vs pending, terminal and other-queue work units excluded, cpu / memory (GiB) / GPU (symbolic and GKE-label types) / TPU, and 6 active work units vs a 2-row first page inside the same snapshot. This shows the aggregation is independent of pagination.
  • Backend tests: user_allocations mapping, ordering and tie-breakers, single-flavor and multi-flavor GPU attribution, and one snapshot per request.
  • UI tests (739/739 pass): specific model, generic parent summing two GPU models, cpu (cores) and memory (GiB), empty-picker hint, no-match message, loading state, and a dashboard test showing that changing the picker updates the bar.
  • Coverage on new files: active_resources.go 100%, user_allocations.go ~100%, jobs.go 89%, canonical.go ~95%, Postgres / Spanner aggregation 82% / 76%. The uncovered lines are database scan and unmarshal error-wrapping paths.
  • Live GKE development environment (deployed via the dev deployment script):
    • Read-only gRPC check of every cluster queue: user_allocations equals the per-user sum of requested units over all pages of jobs. For example, a CPU queue with 14 active jobs (4 × 0.5 cores + 10 × 100 cores, 0.5 GiB each, all SCHEDULING/INITIALIZING) returned cpu 0 running + 1002 pending and memory 0 + 7 GiB. With page_size=5, all three pages returned the same queue-wide totals.
    • Manual UI check (screenshot below, user identity redacted): with cpu picked in "I need" and the 14-job CPU queue selected, the bar above the jobs table reads "Who holds cpu?" — 0 running + 1002 pending cores, with a single user chip "0 running + 1002 pending cores". That matches the jobs table (4 jobs at cpu: 0.5, 10 at cpu: 100).
    • That environment currently has a single user and no active accelerator requests, so the multi-user and GPU/TPU paths are covered by the unit and emulator tests above.

Related Issues / Bugs

Part of #1515

TAG=agy
CONV=4ed50232-a6f2-4c76-aa6f-ef1c3833a550

Add a queue-wide breakdown of requested resources per experiment creator
to GetRunningAndPendingJobs and render it on /resources as a stacked bar
with one chip per user for the first resource picked in "I need".

- storage: AggregateActiveResourcesByAuthor sums reque
8000
sted units by
  creator, canonical resource type and running/pending state across all
  active work units (Spanner and Postgres), and WithReadOnlyWorkUnitSnapshot
  binds it and FetchActiveWorkUnitsByQueues to one read-only snapshot.
- api: GetRunningAndPendingJobsResponse.user_allocations (field 4).
- rod_data_provider: populate user_allocations in the same snapshot as the
  jobs page; memory is normalized to GiB.
- ui: QueueUserAllocationBar above the jobs table; generic parents such as
  nvidia.com/gpu sum all matching models via matchesResourceFilter.

Bug: 582253355
TAG=agy
CONV=4ed50232-a6f2-4c76-aa6f-ef1c3833a550
Remove the Repository-level AggregateActiveResourcesByAuthor (the RPC only
reads through WithReadOnlyWorkUnitSnapshot), duplicate positive-unit filters
and re-canonicalization, nil-callback guards, and the forwarding
formatCompoundAccelerator wrapper; restore dropped comments.

Bug: 582253355
TAG=agy
CONV=4ed50232-a6f2-4c76-aa6f-ef1c3833a550
Bug: 582253355
TAG=agy
CONV=4ed50232-a6f2-4c76-aa6f-ef1c3833a550
…uards

Unexport package-internal canonicalization and state classification
helpers, drop context and nil checks already enforced by callers, remove
an unused mock field and summary property, and drop a zero-total filter
for allocations the backend never emits.

Bug: 582253355
TAG=agy
CONV=4ed50232-a6f2-4c76-aa6f-ef1c3833a550
@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a new feature to aggregate and display per-user resource allocations (running and pending) for jobs in a queue. It adds a new UserResourceAllocation proto message, updates the rod_data_provider to calculate these allocations using a consistent storage snapshot, and adds a new UI component to visualize the data. My feedback suggests adding a nil check for the cluster queue retrieval to improve robustness.

Comment on lines +42 to +45
cq, ok := kueueCache.ClusterQueues.Get(clusterQueueName)
if !ok {
return nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent a potential nil pointer dereference panic, add a nil check for cq in case the cache retrieves a nil entry for the cluster queue name.

Suggested change
cq, ok := kueueCache.ClusterQueues.Get(clusterQueueName)
if !ok {
return nil
}
cq, ok := kueueCache.ClusterQueues.Get(clusterQueueName)
if !ok || cq == nil {
return nil
}

@janzak-dev
Copy link
Copy Markdown
Collaborator Author

Superseded by the stacked PRs #1589 → #1590 → #1591 → #1592 → #1593 (same final code, split so that each layer moves the placeholder data one level down with no dead code). Closing this one in favor of the stack.

@janzak-dev janzak-dev closed this Nov 6, 2026
@janzak-dev
janzak-dev dele 7EF4 ted the feat/rod-user-resource-breakdown branch November 6, 2026 12:13
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.

1 participant

0