Deploy Schedule + GPU Placement & Sharing — sketch

Nguồn: Deploy Schedule plan v4.2 (witty-spinning-lampson.md) + GPS plan v4 (gpu-placement-sharing.md) · model-registry-service · kịch bản mẫu, số liệu minh hoạ (không phải dữ liệu thật). Tuần 2026-10-19 → 2026-10-25, timezone Asia/Ho_Chi_Minh.

1. Kịch bản & khai báo profile

Một GPU pool 10 GPU (cùng flavor/region). A + B cần 14 GPU → không thể chạy đồng thời, phải hoán đổi theo giờ (release-first). Capacity là best-effort (không reservation); muốn B chắc chắn chỉ rơi vào GPU của A thì dùng GPU reservation (mục 8).
A · md-llama70b — chat production, 8 GPU
alias-enabled (chat-prod)target
Giờ hành chính phục vụ người dùng. Stop A cần allow_downtime.
B · md-qwen32b — batch/offline, 6 GPU
no aliastarget
Chạy ngoài giờ, tận dụng GPU rảnh.
C · md-bge-embed — embedding, 2 GPU
không phải target
Không khai báo → scheduler không bao giờ đụng, luôn chạy, chiếm 2 GPU.
// POST /admin/deploy-schedules   (CMP: /api/v1/cmp/deploy-schedules)
{
  "name": "gpu-pool-h100-shared",
  "timezone": "Asia/Ho_Chi_Minh",
  "mode": "enforce",                 // enforce | observe (observe = chỉ tính, không submit)
  "transition_order": "release_first",   // L1: mặc định; acquire_first phải khai báo
  "targets": [                         // ownership: 1 deploy ↔ 1 schedule
    { "model_deploy_id": "md-llama70b",
      "default_state": "stopped",          // ngoài mọi window
      "default_allow_downtime": true },     // A có alias → cần consent
    { "model_deploy_id": "md-qwen32b",
      "default_state": "running" }
  ],
  "windows": [
    { "name": "business-hours", "kind": "recurring",
      "priority": 10,
      "cron": "0 8 * * 1-5", "duration": "10h",  // [08:00, 18:00) T2–T6
      "states": [
        { "model_deploy_id": "md-llama70b", "desired_state": "running" },
        { "model_deploy_id": "md-qwen32b",  "desired_state": "stopped" } ] },
    { "name": "maint-wed", "kind": "one_shot",
      "priority": 100,                     // override thắng business-hours
      "active_from":  "2026-10-21T13:00:00+07:00",
      "active_until": "2026-10-21T15:00:00+07:00",
      "states": [
        { "model_deploy_id": "md-llama70b", "desired_state": "stopped",
          "allow_downtime": true },
        { "model_deploy_id": "md-qwen32b",  "desired_state": "running" } ] }
  ]
}

2. Timeline tuần — window → effective profile → GPU

Click lên timeline để đặt thời điểm.
W maint-wedone_shot · priority 100
W business-hourscron "0 8 * * 1-5" · 10h · prio 10
Effective Amd-llama70b · 8 GPU · alias
Effective Bmd-qwen32b · 6 GPU
C (không phải target)không đụng tới · 2 GPU
GPU desiredpool 10 · đường đỏ = capacity
window one_shot active window recurring active running (nguồn: window) running (nguồn: default_state) stopped không quản lý

TargetDesiredQuyết định bởiGhi chú

Transitions — mỗi lần effective intent của target đổi (mở intent interval mới; nhả trước, chiếm sau)

3. Flow một pass của engine (mỗi 30s, hoặc :apply-now)

1Execution gate deploy_schedule_control.execution: off hoặc đọc lỗi → không submit gì (fail-closed). observe → tính toán + ghi status, không submit.
↓
2pg_try_advisory_lock trên connection riêng (dùng chung với apply-now, 409 nếu bận). Lock chỉ để đỡ tốn công — tính đúng dựa vào fence + CAS + idempotency (prod chạy 3 replica).
↓
3Phân trang từ cursor lưu bền (deploy_schedule_control.cursor), giới hạn số schedule mỗi pass · system actor · snapshot schedule + version.
↓
4Eligibility mỗi passscope, control-plane, không DELETING / FAILED (L2), không fwc / batch / finetune, admin không đụng deploy CMP, chưa hỗ trợ reservation / gpu_sharing (L5) → ineligible{reason} terminal + fix text.
↓
5Evaluator → intent intervalInterval hiện tại == interval đã xử lý? → không mở transition mới; nếu đã converged mà có thao tác tay → drift_detected, không revert (U1). Interval mới (kể cả bị lỡ khi engine down) → lập transition plan.
↓
6Preflight mọi bước chiếm GPU trước bước nhả đầu tiên (chỉ đọc): state, config, admin quota (role allocation), CMP quota + billing, mọi member CMP đang STOPPED, reservation budget (GPS). Lỗi bất kỳ → quota_refused / billing_refused / ineligible và không nhả gì — A không bị stop vô ích.
↓ mỗi step: fence transaction → action submitting → submit với ETag đã chụp + idempotency_key

7a. Release (stop / scale-down)

Fence txSELECT … FOR SHARE schedule · version · state=enabled · gate · claim · CAS status_version · insert action submitting → commit.
Alias guard fail-closedAliasGuardWired() = false hoặc đọc guard lỗi → không stop → alias_blocked.
StopWithOptions(id, etag, {AllowAliasedDowntime, IdempotencyKey}) — consent là tham số typed, không lọc theo error code.
Accepted → action accepted{operation_ids}. Converged khi STOPPED và không còn pod terminating (bằng chứng GPU đã nhả).
Quá STOP timeout → stop_timeout.

7b. Acquire (start / scale-up)

Capacity-wait clockPreviewCapacity (chỉ đọc) = insufficient → capacity_wait; quá nominal_start + 15m (hoặc hết interval) → capacity_unavailable.
Fence tx như 7a.
Start(id, etag, key) / SetReplicaIntent (Admin, mode cố định). Core admission: allocation + flavor quota; reservation budget nếu có (GPS).
Readiness clockconverged khi READY + applied generation + đủ ready replica group (+ witness UUID nếu có reservation).
Quá timeout → readiness_timeout; pod Unschedulable → start_unschedulable (L3: giữ allocation, báo trong status).
↓
8Crash-safeengine nào thấy action còn submitting (crash, mất response, mất lock) → submit lại cùng key; core trả lại Operation cũ hoặc mutate đúng một lần.
↓
9CAS target_status trên (status_version, target_generation) · event khi đổi phase / lỗi · metrics (deployschedule_target_serving, boundary lag, actions by reason).

4. Swap A → B lúc 18:00 (T6) — chuyện gì xảy ra theo thời gian

Boundary 18:00 là lúc mở intent interval mới, không phải lúc B READY. A = STOPPED sớm (đã gửi lệnh xoá) nhưng step stop chỉ converged khi hết pod terminating. B có hai đồng hồ: capacity-wait (≤ 15m) rồi readiness.

5. Phase của target_status (mọi lỗi đều về terminal có reason + fix text — không treo mãi)

6. Các case cảnh báo hiển thị cho operator

alias_blocked

A có alias, window yêu cầu stopped nhưng thiếu allow_downtime — hoặc alias guard chưa wire / đọc lỗi (fail-closed). A tiếp tục chạy; transition dừng ở terminal. :dry-run báo trước. Fix: thêm consent, hoặc sửa wiring.

quota_refused · billing_refused

Preflight thấy B vượt quota / chưa thanh toán → dừng trước khi stop A. Fix: nâng quota / thanh toán rồi :apply-now.

capacity_wait → capacity_unavailable

A đã nhả nhưng GPU chưa publish hoặc workload khác đã chiếm (không có reservation). Chờ tối đa 15m rồi terminal. A không tự start lại.

drift_detected

Operator stop tay A sau khi đã converged → tôn trọng thao tác tay (U1), không start lại tới khi effective intent đổi. Không cần "hold".

start_unschedulable · readiness_timeout

B được nhận nhưng pod không schedule / không READY kịp → terminal. L3: allocation của B được giữ và báo trong status, không tự stop.

partial_execution{members}

CMP endpoint: start lỗi sau 1 member / compensation lỗi / stop lỗi giữa nhóm → báo từng member + receipt. Endpoint đang chạy dở trước đó → ineligible{endpoint_partially_running} (L4).

ineligible{reason}

target_failed (L2), cmp_managed (admin schedule trên deploy CMP), fwc / batch / finetune, gpu_placement_unsupported (L5), scope đổi. Kiểm lại mỗi pass.

target_owned · gate off

Deploy đã thuộc schedule khác → 409 (claim UNIQUE(model_deploy_id)). Gate off: không submit mới; action đã accepted vẫn chạy xong.

7. Kịch bản multi-replica: pool 16 GPU, A có 2 replica → ngoài giờ scale còn 1, dùng 8 GPU dư cho B

// targets + windows (chỉ phần khác so với mục 1)
"targets": [
  { "model_deploy_id": "md-llama70b",      // 8 GPU / replica
    "default_state": "running",
    "default_replicas": 1 },              // ngoài giờ: 1 replica
  { "model_deploy_id": "md-qwen-8gpu",
    "default_state": "running" }
],
"windows": [
  { "name": "business-hours", "cron": "0 8 * * 1-5", "duration": "10h",
    "states": [
      { "model_deploy_id": "md-llama70b", "desired_state": "running",
        "replicas": 2 },                    // pin desired=min=max=2
      { "model_deploy_id": "md-qwen-8gpu", "desired_state": "stopped" } ] }
]
// "replicas" vắng mặt → scheduler không đụng tới scale
// replicas = 0 bị reject → muốn về 0 thì dùng "stopped"
Điểm hay của kịch bản này: A không bao giờ về 0, nên không có downtime và không cần allow_downtime kể cả khi A có alias. Scale 2→1 chỉ giảm capacity phục vụ, scheduler ghi event cảnh báo capacity_reduced.

Quy tắc thứ tự tổng quát: làm hết các bước nhả GPU (stop, scale-down) trước, rồi mới tới các bước chiếm GPU (start, scale-up). Mỗi bước chiếm GPU phải qua PreviewCapacity cho phần tăng thêm (delta), không tính lại toàn bộ deploy.

Node / GPU layout qua các mốc

① 08:00–18:00 · giờ hành chính

A = 2 replica · 16/16 GPU
node-1 · 8 GPU
A1
A1
A1
A1
A1
A1
A1
A1
node-2 · 8 GPU
A2
A2
A2
A2
A2
A2
A2
A2

② 18:00 · scale A 2→1

A vẫn phục vụ bằng 1 replica · không downtime
node-1
A1
A1
A1
A1
A1
A1
A1
A1
node-2 · A2 terminating

③ ~18:02 · start B

capacity publish → preview fits → Start(B)
node-1
A1
A1
A1
A1
A1
A1
A1
A1
node-2
B
B
B
B
B
B
B
B

④ 08:00 hôm sau · đảo ngược

stop B → chờ GPU trống → scale A 1→2
node-1
A1
A1
A1
A1
A1
A1
A1
A1
node-2 · B stopped → free
free
free
free
free
free
free
free
free
A replicaBpod đang terminate (GPU chưa trống)trống

18:00 — scale-down A rồi start B

  1. Profile mới: A running, replicas=1, B running. Bước nhả: scale A. Bước chiếm: start B.
  2. SetReplicaIntent(A, 1, key) (core, U4): cập nhật cùng lúc workload spec (spec.components[serve].replicas 2→1) và Resources.roles[serve].replicas = {1,1,1} — chỉ sửa Resources thì workload không đổi.
  3. Hạ cả max là bắt buộc: deploy PINNED giữ chỗ theo ceiling (MaxReplicas). Giữ max=2 thì accounting vẫn tính A chiếm 16 GPU, B bị từ chối.
  4. A → UPDATING → DGD xoá 1 replica. Converged khi applied generation, desired = 1, ready group = 1 và không còn pod terminating (ObserveReplicas, không đếm 0 khi lỗi).
  5. Capacity publisher thấy node-2 trống (≈60s) → PreviewCapacity(B) = fits (flavorfit theo từng node, tránh phân mảnh).
  6. Fence tx → action → Start(B, key) → UPDATING → READY → converged. Không fits trong 15m → capacity_unavailable.

08:00 — stop B rồi scale-up A

  1. Profile: A replicas=2, B stopped. Bước nhả: stop B. Bước chiếm: scale A 1→2.
  2. Stop(B) (B không alias → không cần consent).
  3. Trong lúc chờ, A vẫn phục vụ bằng 1 replica → suốt quá trình không có downtime.
  4. B STOPPED + capacity publish → PreviewCapacity(A, +1 replica = 8 GPU) = fits. Chưa fits thì A ở capacity_wait với 1 replica.
  5. SetReplicaIntent(A, 2, key): workload + roles {2,2,2}. Role accounting có thể từ chối (quota flavor) → quota_refused (terminal; preflight đã chặn trước khi stop B nếu xác định được).
  6. Observe RunningReplicas(A) == 2 và READY → converged.
Deploy đang STOPPED mà profile yêu cầu running với số replica khác: SetReplicaIntent khi STOPPED chỉ lưu (persist-only), lấy ETag mới, rồi mới Start. Start dùng spec đang lưu.
Với GPS: pool 16 GPU = 2 reservation (mỗi node một reservation, vì reservation chỉ 1 node). A bind exclusive{8/replica} vào R1+R2; ngoài giờ B bind exclusive{8} vào R2 → B chỉ được phép rơi vào đúng 8 UUID A vừa nhả, không workload ngoài nào chiếm được (Track H: scheduler ép; Track S: node pin + witness). Đây là "eligibility được bảo vệ", không phải bảo đảm B chắc chắn READY.

Loại deploy nào schedule được phép scale (đối chiếu code main@9dfa4a37)

GA (General Availability) = bản phát hành chính thức của plan này, tức sau G8: đã pilot Admin trên sv16, rồi HLC, rồi bật cho production. Sau GA = hoãn sang epic/plan sau (plan ghi là Deferred), chưa ước lượng. Từ chối = API trả unsupported_scale_for_mode khi tạo/sửa schedule. Theo U4, GA chỉ scale cho Admin, replica cố định, qua thao tác mới ở core SetReplicaIntent (G6).
Kiểu deployCode hiện tại scale thế nàoVấn đềTrong GA?
DGD aggregated, role serve, replica cố định (không có scalingAdapter)Patch = WorkloadPatch (component replicas) + Resources roles. Precedent: fireworks-compat patchScaling / WithRoleReplicasPhải gửi lại toàn bộ spec.components (merge patch thay cả list) → SetReplicaIntent gói việc này ở coreCó
Như trên nhưng multinode (nodesPerReplica > 1)Như trênNhả / chiếm GPU theo cả nhóm node; convergence đếm theo replica group, preview tính theo nhómCó
Deployment / LWS không có HPAReplica trong workload specNhư DGD aggregatedCó
DGD có scalingAdapter (SLA Planner)Apply mirror replica từ live (mirrorAdapterReplicas); operator chặn sửa trực tiếpPatch spec không có tác dụng; phải hạ policy của Planner (DynamoScaling.MaxReplicas) hoặc qua DGDSASau GA
Deployment / LWS + HPAholdAutoscaledReplicas: chỉ nâng lên số liveHPA đang giữ 2 thì scale-down không ăn; phải hạ max của HPASau GA
KServe / Knative (concurrency, scale-to-zero)min/max annotationCần định nghĩa riêng min/max theo windowSau GA
Disagg prefill / decode2 role, fixed single-nodereplicas: 1 nghĩa là gì còn mơ hồ (prefill? decode? cả hai?)Từ chối
Endpoint CMP (mọi kiểu)—Không có điểm kiểm quota khi scale-up ở orchestratorSau GA (CMP chỉ start/stop)
Kiểu không thuộc dòng "Có" mà window khai báo replicas → unsupported_scale_for_mode khi tạo/sửa schedule, kiểm tra lại mỗi pass. Không âm thầm diễn giải. Start/stop vẫn dùng được cho mọi kiểu deploy hợp lệ.

8. GPU reservation + time-slicing — node kiểu gpu42 (8× H200, Track S)

Reservation rsv-gpu42-shared giữ 1 GPU (viền đậm) trên hla-worker-042, namespace cmp-<project>. 3 member shared, replicas=1, KAI NonMemoryEnforced (compute time-sliced, VRAM policy none — không có VRAM cap). Số liệu bộ nhớ lấy từ receipt gpu42; UUID rút gọn.

Member & ngân sách VRAM (kéo để thử)

Tổng memory_mib các member đang chạy trên GPU 0 (H200 = 143 771 MiB, headroom 0.95 → 136 582 MiB)

MRS kiểm tra gì (Track S)

  1. Admission trong core Start/SetReplicaIntent, tuần tự theo generation của reservation: Σ memory_mib của member đang chạy thật + action đang bay + action đề xuất ≤ budget. Vượt → terminal reservation_overcommitted. Start tay (U1) cũng qua bước này.
  2. --gpu-memory-utilization của vLLM do MRS suy ra từ memory_mib; user args mâu thuẫn → reject.
  3. Render: schedulerName qua SchedulingPolicy (không hard-code), queue do server suy ra, node pin = node của reservation, annotation gpu-memory = memory_mib.
  4. Holder member: nếu mọi member dừng, KAI xoá reservation pod và GPU trống cho người khác → MRS giữ một pod fractional tối thiểu để GPU group sống qua swap (S0 kiểm).
  5. Witness đọc qua API (annotation binder nvidia.com/container.<c>.gpus.devices nếu S0 xác nhận, không thì DCGM): UUID ∈ reservation → converged; ngoài reservation → placement_mismatch. Không exec nvidia-smi.
  6. Quota/billing: reservation tính 1 GPU; 3 member chỉ tính CPU/RAM (live-base hiện đếm nhầm 3 GPU).
Rủi ro còn lại với none: budget chỉ là luật admission của MRS — vLLM vẫn có thể cấp phát vượt và làm neighbour OOM. Tuỳ chọn guard = KAI HamiCore (HAMi-core chặn VRAM, compute vẫn time-slice) — chỉ bật sau khi S0 test với vLLM (quyết định T2).

Schedule trên reservation: giờ hành chính A + C, ngoài giờ B + C

Thứ tự swap 18:00:

9. Track S vs Track H — pin tới đâu, ai ép, witness lấy từ đâu

Track S — làm ngay (K8s 1.35.4, KAI v0.18.0, device plugin)

  1. Reservation = sổ cái MRS: node + GPU UUID + budget.
  2. Pod: kai-scheduler, gpu-memory, nodeSelector = node của reservation.
  3. KAI binpack chọn GPU trên node (ưu tiên GPU group đang share) — không chọn được UUID trước.
  4. Witness UUID qua API sau khi bind.
  5. Khớp → converged · lệch → placement_mismatch (terminal, có fix text, không tự sửa).
node pin cứngbudget admissionUUID: kiểm chứng sauVRAM: không cap

Track H — pin cứng UUID (DRA)

  1. Điều kiện: K8s 1.36 (DRAConsumableCapacity beta → HAMi-DRA) hoặc NVIDIA DRA GPU plugin hỗ trợ chính thức. S0 chọn một.
  2. ResourceClaim chọn device theo UUID (CEL / annotation HAMi-DRA), time-slicing.
  3. Scheduler ép UUID; VAP chặn claim/pod ngoài reservation.
  4. Witness = kết quả allocation của claim (⊆ tập UUID được giao).
  5. Capacity đọc từ ResourceSlice (devicesnapshot) thay cho nvidia.com/gpu.
UUID ép bởi schedulerVAP barriercần nâng K8s / driverKAI+DRA chưa kiểm chứng
Cơ chếChọn UUID trướcVRAM capTrạng thái trên HLA
KAI NonMemoryEnforced (hiện tại)KhôngKhôngĐang chạy (gpu42)
KAI HamiCore (≥ v0.17)KhôngCó (CUDA intercept)Chưa cài kai-resource-isolator
HAMi-DRACó (annotation)CóCần DRAConsumableCapacity (alpha trên 1.35) + CDI
NVIDIA DRA driverCó (CEL)Không (time-slicing)GPU allocation "not yet officially supported"

10. Lộ trình & quyết định

L0Đối soát live-base 7f73589f ↔ main; giữ 000214/215; check DB read-only (cần duyệt) → schedule M1–M2, release R0
S0Spike Track S trên version HLA: witness, binpack, holder, util formula, HamiCore+vLLM
P1Core: reservation, lifecycle, rebind, admission, accounting/billing
P2Track S render · holder · witness · intruder
P3Tích hợp schedule
P4CMP (exclusive + timeslice theo T3)
P5Ops: alert, dashboard UUID, playbook
Track Hsau K8s 1.36 / DRA GPU plugin GA
#Quyết địnhTrạng thái
Q1Re-pin = API có audit, fence theo generationđã chốt
Q2Reservation một phần node sau khi S0 chứng minh barrierđã chốt
Q3Bill reservation-hour kể cả idle; dừng ở DEGRADED/RELEASINGđã chốt
Q4Raw CMP DGD đi qua quota + billingđã chốt
Q5Bỏ queue/node_name do tenant truyềnđã chốt
—Không MPS, không MIG; time-slicingđã chốt
T1Track S ngay; Track H sau K8s 1.36chờ user
T2VRAM none như hiện tại; HamiCore opt-in sau S0chờ user
T3CMP time-slicing chỉ trong reservation của chính project, có ackchờ user