Chuyển tới nội dung chính

Workload

Part of the Scheduler feature area (axap-gpu-volcano-ui, MFE id 'scheduler'). Sibling docs: gpu-overview, gpu-inventory, timetable.

Overview

Workload (schedulerWorkload) is a filterable table of Volcano PodGroup workloads across clusters/projects/pools, with a “Deploy Workload” guide modal. It lives at modules/volcano/dashboard/pages/Workload/index.vue with a helper module modules/volcano/dashboard/pages/Workload/workloadHelpers.ts.

The backend is Volcano via the FastAPI service axap-gpu-volcano (prefix /scheduler/v1), plus zmp-core-api (/core/v1) for the Project filter’s option list.

A demo mode is baked into this codebase and is ON by default (modules/volcano/dashboard/demo/isDemo.ts) — disabled only per-tab via ?demo=0. In demo mode this page uses demoWorkloads() from src/modules/volcano/dashboard/demo/demoMetrics.ts for both the workload list and the derived filter options (pools/projects), instead of calling the real backend.

Route Design

Route constants: constants/routeNames/index.ts, paths: constants/routes/index.ts, lazy components: pages/index.ts. Registered in both the system and project route tables built by buildRoutes() in router/index.ts.

Route nameRaw pathSystem pathProject pathComponent
ROUTE_NAMES.WORKLOAD (schedulerWorkload)/workloads/system/scheduler/workloads/proj/:projectId/scheduler/workloadsWorkload/index.vue — router/index.ts

No hidden/drill-down routes belong to this screen itself; it does, however, link out to two things that are not routes inside this repo: (a) a Model Manager “Create Model” page (external app, via goModelDeploy), and (b) nothing else — the “커스텀 배포하기” branch stays in-modal (renders a YAML example, does not navigate).

Route mechanics common to the whole Scheduler MFE (system vs. project route table selection, getHostProjectId() scope-locking) are the same as documented in gpu-overview.

UI Structure

Workload → Workload/index.vue
├── Page head: title + description + "+ 워크로드 배포" button
├── Filter bar: 클러스터 / 프로젝트(전체+list) / GPU 풀(전체+list) /
│ 워크로드 타입(Inference/RayJob) / GPU 타입(FULL/FULL_NVLINK/MIG/TS) + 검색 + active-filter tags
├── Loading / Error states
├── Workload table (one row per PodGroup):
│ 클러스터 | 프로젝트 | GPU 풀 | 워크로드(name+status badge) | 워크로드 타입 | GPU 타입 | GPU 자원 | 배포내역(created+age+ID)
└── "워크로드 배포" 2-step modal (Teleport to body)
├── Step 1: 모델 매니저로 배포하기 (external link-out) | 커스텀 배포하기 (→ Step 2)
└── Step 2: 워크로드 종류 tabs + GPU 타입 select + type-specific resource inputs
+ read-only YAML 예시 (copy-to-clipboard)

Component Design

pages/Workload/index.vue is a single large page component (no extracted child components beyond the inline modal markup):

  • Filter bar state: filters (reactive: cluster, project, pool, wlType, gpuType, text); options are derived reactively:
    • clusterOptions — union of resolvedCluster (from resolveCluster()) and profileStore.clusters.
    • projectOptions/poolOptions — loaded async by loadFilters() (see API Design), or derived from demoWorkloads() in demo mode.
    • hasActiveFilters — true if any of project/pool/wlType/gpuType is set (drives the “active filter tags”/“모두 지우기” row).
  • Table rows are derived from workloads.value filtered by filters, mapped through modules/volcano/dashboard/pages/Workload/workloadHelpers.ts helpers: workloadTypeOf (Inference/RayJob classification), gpuTypeOf (FULL/FULL_NVLINK/MIG/TS classification), gpuResourceText (human-readable resource string), workloadIdOf (deploy-history ID), formatDateTime/relativeAge (배포내역 column).
  • Deploy modal (createOpen, step ref 1|2, form reactive: wlKind, gpuType, gpuCount, migProfile, migCount, vgpuCores, vgpuMemGb):
    • Step 1 choice cards: “모델 매니저로 배포하기” (goModelDeploy()) vs. “커스텀 배포하기” (step = 2).
    • Step 2 renders a computed exampleManifest (YAML text) that changes based on form.wlKind/form.gpuType/resource inputs, with a copyExample() clipboard action (copied flag flips the button label to “복사됨 ✓”).
    • No backend call is made from this modal — it is guide-only (see Business Logic).

API Design

All calls use services.axiosInstance from @cloudz-mp/zmp-base-ui.

Base path resolution

Shared with the other screens — see gpu-overview for resolveRealm()/ resolveCluster()/getRealmClusterPath()/getRealmPath() precedence rules (modules/volcano/dashboard/apis/index.ts).

Endpoints used by Workload

FunctionMethod & pathNotes
fetchWorkloads('', '', resolvedCluster)GET {realmCluster}/workloads (cluster explicit, no namespace/pool filter)called from loadWorkloads(); demo mode substitutes demoWorkloads()
fetchProjects()GET {corePath}/projects?page=0&size=65535called from loadFilters() to populate the 프로젝트 select; failure is caught independently so the workload list still renders
fetchGpuPoolList()derives from fetchVolcanoQueues() (maps q.metadata.name → {label, value})called from loadFilters() to populate the GPU 풀 select; failure caught independently

Both loadWorkloads() and loadFilters() are invoked in parallel from onMounted() (no await between them), so the table can render before filter options finish loading.

No mutation endpoints (create/update/delete workload) are called from this screen — see Business Logic rule “Deploy Workload modal is guide-only”.

Dev-time proxy: /scheduler/v1 → axap-gpu-volcano (localhost:8000); /core/v1 → zmp-core-api (localhost:8081) — vite.config.ts.

State Management

  • No Pinia store is used by this page — workloads, filters, loading, error, projectOptions, poolOptions, createOpen/step/form/copied are all page-local ref/reactive state in Workload/index.vue.
  • useProfileStore (stores/useProfileStore.ts) is read for profileStore.clusters (merged into clusterOptions) — no direct scope-filter writes happen from this page (there is no ScopeFilter component on this screen; filters are page-local <select>s, not the shared cross-page ScopeFilter).

Business Logic

  1. Workload status derivation — same rule as GPU Overview (see gpu-overview item 1): status badge (statusTone/statusLabel) is driven by the normalized VolcanoWorkload.status from normalizeWorkload().
  2. Deploy Workload modal is guide-only — no create-workload POST call exists anywhere in Workload/index.vue; the modal only (a) redirects to Model Manager (goModelDeploy), or (b) renders a copyable YAML annotation example based on the selected workload/GPU type. The actual deployment happens outside this app.
  3. GPU-type-specific example manifest fields: FULL/FULL_NVLINK → GPU count field (nvidia.com/gpu); MIG → MIG profile (MIG_PROFILES list) + count; else (Time-Slicing) → vGPU cores (%) + vGPU memory (GB) — drives which input row is shown in Step 2 and what the generated exampleManifest contains.
  4. Independent filter-option failure isolation — fetchProjects() and fetchGpuPoolList() are each wrapped in their own try/catch inside loadFilters(), so a failure of one (e.g. core-api down) does not block the other, nor does it block loadWorkloads()’s table render.
  5. Cluster/GPU-pool/project filtering is entirely client-side — fetchWorkloads is called once per the resolved cluster (no server-side project/pool query params passed); all filters.* selects narrow the already-fetched workloads.value in memory via filteredRows.
  6. Demo mode is default-ON — same cross-cutting rule as the other three screens; isDemoMode() swaps both the workload list (demoWorkloads()) and filter options (derived from that same demo dataset, to stay in sync) in loadWorkloads()/ loadFilters().

Data Flow

flowchart TD
R[/system/scheduler/workloads/] --> P[Workload/index.vue]
P -->|onMounted| L1[loadWorkloads]
P -->|onMounted| L2[loadFilters]
L1 --> DEMO1{isDemoMode?}
DEMO1 -->|yes| D1[demoWorkloads]
DEMO1 -->|no| A1[apis: fetchWorkloads]
A1 --> BE[(axap-gpu-volcano /scheduler/v1)]
L2 --> DEMO2{isDemoMode?}
DEMO2 -->|yes| D2[demoWorkloads -> derive pool/project options]
DEMO2 -->|no| A2[apis: fetchProjects]
DEMO2 -->|no| A3[apis: fetchGpuPoolList -> fetchVolcanoQueues]
A2 --> BE2[(zmp-core-api /core/v1)]
A3 --> BE
P --> T[filteredRows table render]
P -->|+ 워크로드 배포| M[Deploy modal - guide only, no API call]

Dependency Graph

flowchart LR
Workload --> workloadHelpers["workloadHelpers.ts"]
Workload --> useProfileStore
Workload --> apisIndex["apis/index.ts (scheduler/v1, core/v1)"]
apisIndex --> useProfileStore

Sequence Flow

Workload list load

sequenceDiagram
actor User
participant Page as Workload/index.vue
participant API as apis/index.ts
participant BE as axap-gpu-volcano
participant CoreBE as zmp-core-api

User->>Page: navigate to /system/scheduler/workloads
Page->>Page: onMounted -> loadWorkloads() + loadFilters() (parallel)
alt demo mode
Page->>Page: workloads = demoWorkloads()
else real backend
Page->>API: fetchWorkloads('', '', resolvedCluster)
API->>BE: GET /scheduler/v1/realms/{r}/clusters/{c}/workloads
BE-->>API: raw items
API->>API: normalizeWorkload() per item
API-->>Page: VolcanoWorkload[]
end
par filter options
Page->>API: fetchProjects()
API->>CoreBE: GET /core/v1/realms/{r}/projects
CoreBE-->>API: projects
API-->>Page: projectOptions
and
Page->>API: fetchGpuPoolList()
API->>API: fetchVolcanoQueues()
API->>BE: GET .../volcano-queues
BE-->>API: queues
API-->>Page: poolOptions
end
Page-->>User: renders filter bar + table (filteredRows)

Deploy Workload modal (guide-only, no API call)

sequenceDiagram
actor User
participant Page as Workload/index.vue

User->>Page: click "+ 워크로드 배포"
Page->>Page: createOpen = true, step = 1
alt choose 모델 매니저로 배포하기
User->>Page: click "모델 매니저로 배포하기"
Page->>Page: goModelDeploy() (external link-out)
else choose 커스텀 배포하기
User->>Page: click "커스텀 배포하기"
Page->>Page: step = 2
User->>Page: select 워크로드 종류 / GPU 타입 / resource inputs
Page->>Page: exampleManifest recomputed (client-side only)
User->>Page: click "예시 복사"
Page->>Page: copyExample() -> clipboard write, copied = true
end
Page-->>User: modal remains a static guide; no network request fired

Key Findings

  • This screen has no dedicated “create workload” backend endpoint call — despite having a prominent “+ 워크로드 배포” button, the entire flow is either an external link-out or a static documentation/example generator.
  • No shared ScopeFilter component — unlike GPU Overview and Time Table, this page implements its own inline filter &lt;select&gt;s rather than reusing components/ScopeFilter/index.vue, so filter UX (e.g. “All” semantics, locked project in project mode) is not guaranteed to be identical to the other screens.
  • All cluster/project/pool/type filtering happens client-side after a single unfiltered fetchWorkloads call for the resolved cluster — this page does not use the namespace/gpuPool query params that fetchWorkloads() supports.

Risks / Technical Debt

  • No project-mode scope lock observed — this page does not call getHostProjectId() (unlike GPU Overview/Time Table), so it is unclear whether project-mode users see only their project’s workloads or all workloads with just a project filter defaulting to their id. Treat project-mode behavior on this screen as Not Found in Code / unconfirmed.
  • No error normalizer — error.value = e instanceof Error ? e.message : '알 수 없는 오류' in loadWorkloads(); loadFilters()’s two try/catch blocks only console.error and leave options empty on failure, with no user-facing error surfaced for that partial failure.
  • Single-cluster only — fetchWorkloads('', '', resolvedCluster) always targets one resolved cluster; the “클러스터” filter’s non-active-cluster options (from profileStore.clusters) narrow an already cluster-scoped dataset rather than triggering a re-fetch against another cluster (re-fetch-on-cluster-change behavior was not confirmed in this pass).

Assumptions

  • The exact filteredRows computed filtering logic (how filters.text free-text search matches across name/namespace/project/pool) was referenced by the filter placeholder text (“워크로드 · 네임스페이스 · 프로젝트 · 풀 검색”) but its implementation was Not Found in Code in this pass (not read).
  • Whether changing the “클러스터” <select> triggers a re-fetch of fetchWorkloads for the newly selected cluster, or only filters the existing single-cluster dataset, is Not Found in Code / unconfirmed in this pass.
  • WORKLOAD_KINDS and MIG_PROFILES constants (used in the deploy modal) were referenced but their definitions were not opened — treat their exact values as Not Found in Code here.