DESIGN NOTE — 2026-04-10

멀티 GPU 스케줄링 정책 — 설계 노트

날짜: 2026-04-10 대상: JPU-Share scheduler v2 (placement + reservation + lazy 가산) 상태: 정책 확정, 코드 구현 대기 관련 문서: 운영 정책 / 멀티 GPU

이 문서는 멀티 GPU 잡 스케줄링 정책을 새로 결정하는 과정에서 우리가 검토한 모든 옵션과 채택/기각 이유를 기록합니다. 운영 정책 페이지가 "지금 어떻게 동작하는가"를 설명한다면, 이 노트는 "왜 그렇게 결정했는가"를 설명합니다. 향후 정책 변경을 검토할 때 이 문서가 이전 논의의 출발점이 됩니다.

이 노트는 최대한 상세를 목표로 작성되었습니다. 한 줄짜리 요약이 필요하면 운영 정책 페이지를 보세요.

BACKGROUND

1. 배경

1.1 직전 코드의 동작

이번 작업 전 JPU-Share 스케줄러는 head-of-line skip 방식이었습니다. app/services/scheduler.py_select_best_job이 매 5초마다:

  1. 모든 auto_assign 티어를 sort_order DESC로 정렬
  2. 각 티어의 Slurm 파티션에 sinfo로 idle GPU 수를 물어본다
  3. 대기 중인 잡을 fair-share(lab.cumulative_usage ASC)로 정렬
  4. 1순위 잡부터 차례로, 각 잡에 대해 매칭 가능한 첫 티어를 찾는다
  5. 매칭에 성공한 첫 잡을 dispatch하고 종료 (1주기 1건)

이 방식의 문제는 1순위 잡이 매칭 안 되면 그냥 건너뛴다는 점입니다. 예를 들어 1순위가 num_gpus=2인데 어떤 티어도 idle 2장이 없으면, 그 잡은 skip되고 다음 잡(num_gpus=1)이 dispatch됩니다. 큐 순서가 fair-share 점수와 일치하지 않게 되어 멀티 GPU 잡의 starvation이 잠재적으로 발생할 수 있었습니다.

1.2 발견한 추가 문제 3가지

문제설명
학생 num_gpus 제한 학생은 num_gpus > 1을 못 쓰게 막아둔 코드(app/api/routes/jobs.py:79)가 있었음. 명확한 정책 근거 없이 그냥 박혀있었음. → 이번에 제거
num_gpus 곱 누락 버그 (usage 계산) record_job_usagetflops × duration만 곱하고 num_gpus를 곱하지 않음. 즉 num_gpus=2 잡과 num_gpus=1 잡이 같은 시간 실행하면 같은 사용량으로 카운팅됨. 명백한 버그
노드 분산 false positive parse_sinfo_gpu_output이 파티션 전체에서 idle GPU 수를 단순 합산. 같은 티어에 노드 A 1장 + 노드 B 1장이면 idle=2로 인식되지만, sbatch는 단일 노드에서 2장을 못 모아 잡이 PENDING(Resources)로 남음. 현재 staging은 티어당 노드 1개라 표면화되지 않은 잠재 버그

1.3 목표

이번 정책 재설계의 4가지 목표:

  1. GPU utilization 최대화 — GPU가 노는 시간이 0에 가까워야 함
  2. 단순한 스케줄링 — 새 enum/테이블/worker 추가를 최소화. 알고리즘이 한 함수에 들어갈 정도
  3. 공정성 — fair-share가 진정으로 작동. multi 잡이 starve되지 않고, 자원 남용은 자동 페널티
  4. 사용자 직관과 일치 — "왜 내 잡이 안 가지?"의 답이 항상 명확

EVOLUTION

2. 결정 흐름 (시간 순)

이 섹션은 결정이 어떤 순서로 합의되었는지를 시간 순으로 기록합니다. 각 단계의 핵심 질문, 답, 이유를 명시합니다.

STEP 1 학생 num_gpus 제한 폐지 → 역할은 GPU 사용에 무관

발견: app/api/routes/jobs.py:79에 학생은 num_gpus > 1 제출 시 403을 반환하는 가드가 있었음. 어떤 정책 문서나 RBAC 명세에도 근거가 없음. MVP 시절의 잔재로 추정.

결정: 즉시 제거. 학생도 num_gpus 1~8 범위에서 자유롭게 제출 가능. 실제 상한은 티어의 max_gpus로 제한.

이유: 명문화되지 않은 정책은 정책이 아님. 운영자 임의 판단 코드는 사용자 신뢰를 떨어뜨림. 진짜 제한이 필요하면 정책으로 명시하고 코드와 일치시켜야 함.

일반화된 원칙: 이 결정으로 우리는 더 큰 원칙을 명시화했습니다 — 사용자 역할(Student / Professor / Admin)은 조직 관리용 개념이고, GPU 자원 사용 권한에는 역할에 따른 차이가 없다. 역할은 연구실 멤버십, 멤버 관리, 시스템 운영 등 조직 관리에만 적용됩니다. GPU 매칭 우선순위는 오직 fair-share(연구실 누적 사용량)로만 결정됩니다.

STEP 2 단일 큐 vs 분리 큐

질문: 멀티 GPU 잡과 싱글 GPU 잡을 같은 큐에서 처리할 것인가, 분리할 것인가?

옵션장점단점
단일 큐 (현행 유지) 관리 단순. 정렬 키 1개. fair-share 일관성 매칭 시점에 multi/single 차이를 어떻게 처리할지 결정 필요
tier별 큐 티어별 자원 보호 가능 큐 N개 관리, fair-share를 큐 간에 어떻게 조정할지 복잡
multi/single 분리 큐 multi 잡 우선권 부여 가능 큐 간 균형 정책이 새 매직 넘버. starvation 양방향 가능

결정: 단일 큐 유지.

이유: 코드는 이미 단일 큐였고(state == GLOBAL_QUEUE_WAIT), 분리하면 큐 간 정책이라는 새로운 차원이 추가되어 복잡도가 폭증함. 단순함 우선 원칙.

STEP 3 Dispatch 모델: 잡 중심 vs device 중심 vs 노드 중심 vs placement

질문: 매칭 알고리즘의 outer loop를 무엇으로 할 것인가?

처음에는 "잡 중심"(현행)과 "device 중심"의 두 가지를 검토했습니다.

잡 중심 (현행):  for job in fair_share_sorted_jobs:
                   for tier in tiers:
                     if matches: dispatch
                 → 1순위 잡이 안 맞으면 skip → 다음 잡

device 중심:    for device in idle_devices:
                   for job in fair_share_sorted_jobs:
                     if fits: dispatch
                 → 빈 자원에 들어갈 수 있는 가장 위 잡을 찾음

그런데 사용자가 "device가 비었을 때 ... 순위대로 검색"이라고 한 표현을 분석해보니, 진짜 단위는 "노드"여야 한다는 게 드러났습니다 — 멀티 GPU 잡은 Slurm 기본 동작상 단일 노드에서 모든 GPU를 받아야 하기 때문입니다.

하지만 또 한 단계 더 들어가보니, "노드"도 정확하지 않았습니다 — 한 노드에 이종 GPU가 섞여 있을 수 있기 때문입니다.

예: 가상의 노드에 4090×4 + 5090×4
    → 노드 단위로 보면 capacity=8
    → 그런데 num_gpus=8 잡은 4090과 5090을 섞어 받지 못함
    → 노드 단위는 잘못된 단위

진짜 단위는 "같은 노드 + 같은 GPU 모델"의 묶음입니다. 용어를 "set"으로 시작해서 최종적으로 placement로 확정.

결정: placement 단위 매칭. (node_name, tier_id)를 키로 하는 derived view.

이유:

STEP 4 용어: GPU와 placement

잠시 "slot = 1 GPU"라는 추상화를 도입했었으나, 사용자가 "그냥 GPU라고 하면 되지 않냐"고 지적.

결정: 두 단어만 사용 — GPU(1 디바이스)와 placement(같은 노드 + 같은 모델 묶음). placement만 새 개념이고 나머지는 다 익숙한 용어.

이유: 추상화는 새로운 의미를 만들 때만 가치가 있음. "slot"은 "GPU"를 다른 이름으로 부르는 것뿐이라 가치 없음.

STEP 5 매칭 조건의 2단계 분리: fit vs available

잡과 placement의 매칭 조건을 두 단계로 분리:

조건정의평가 시점
fit 잡이 이 placement에서 원리상 실행 가능 — tier ≥ minimum_tier AND capacity ≥ num_gpus 제출 시 한 번 (정적)
available 지금 즉시 dispatch 가능 — fit AND idle ≥ num_gpus 매 5초 (동적)

이유: fit은 정적이므로 잡 제출 시점에 한 번 평가. fit한 placement가 0개면 즉시 거부(STEP 6 참고). available은 동적이고 reservation 결정과 연결됨. 두 단계로 분리하면 알고리즘이 자연스럽게 정리됨.

STEP 6 fit placement 0개 잡의 거부

발견: 사용자가 num_gpus=4를 요청했는데 모든 placement의 capacity가 2라면 어떤 매칭도 불가능. 이전 코드는 이 잡을 그냥 큐에 두고 영원히 대기.

결정: 제출 시점에 EXISTS (SELECT 1 FROM placements WHERE fit) 체크. 없으면 즉시 422 NO_FIT_PLACEMENT 반환.

이유: 사용자에게 영문도 모르고 무한 대기시키는 것보다 즉시 명확한 에러 메시지 반환이 압도적으로 좋음. 운영 부담도 감소.

STEP 7 1순위 잡 보장 — head-of-line skip 폐지, reservation 도입

핵심 결정: "큐 순서를 절대 어기지 않는다." fair-share 1순위 잡은 매칭 안 돼도 다음 차례를 양보하지 않음. 대신 가용한 GPU를 선점(reservation)해두고 추가 GPU를 기다림.

이유:

STEP 8 Swap rule — 더 빠른 placement에서 즉시 dispatch

사용자가 도입을 제안한 옵션. Reservation 잡이 placement A에서 1/2장 선점 중인데, 같은 주기에 placement B에서 갑자기 2장이 동시에 idle해진다면?

매 주기 dispatch 알고리즘 4-(a):
  잡의 fit placement 중 available한 게 있으면:
    → 즉시 dispatch
    → 만약 이 잡이 다른 placement에서 reservation 보유 중이었으면
      그 reservation 해제 (★ swap)

결정: 채택. 알고리즘 4-(a)에서 자연스럽게 처리됨.

이유: Reservation은 best-effort 임시 할당. 더 빠른 dispatch 경로가 생기면 그쪽으로 점프하는 것이 latency 측면에서 무조건 이득. 코드 복잡도 증가도 거의 0 (algorithm 4-(a)가 항상 4-(c)보다 먼저 실행됨).

STEP 9 LLM 백필과 reservation의 상호작용

핵심 통찰: Reservation의 가장 큰 약점은 "선점된 GPU가 노는 시간 동안의 utilization 손실"인데, JPU-Share는 LLM 백필이 있으니 이 손실이 자동으로 0이 됩니다.

Reservation은 "다른 연구 잡 배정 금지" 마크일 뿐, LLM 백필은 그 GPU 위에서 그대로 동작합니다. dispatch 시점이 되면 LLM에게 종료 명령을 보내고 sbatch를 실행합니다.

결정:

이유: 우리 코드 단순화 + 책임 분리. LLM 라우터는 어차피 모델 dispatch 관리를 하고 있으므로 종료 상태 처리도 그쪽이 자연스러움.

STEP 10 사용량 가산: Eager vs Lazy

이 단계가 가장 미묘했고 두 번 뒤집혔습니다.

Eager 가산 첫 안: reservation 진행 중 매 dispatch 주기마다 (이전 주기부터 지금까지의) 선점 비용을 lab.cumulative_usage에 incremental 가산. 자기교정 메커니즘.

사용자가 발견한 문제: 이 방식은 multi-GPU 잡이 single 잡보다 부가적인 비용이 누적된다 — 사용자가 single로 쪼개려는 인센티브 distortion이 생긴다. 더 큰 문제: 자기 잡의 reservation 비용이 자기 lab usage를 끌어올려 자기를 1순위에서 밀어내는 자살적 자기교정이 발생. "내가 reservation을 시작했는데 그것 때문에 내가 1순위에서 밀려난다"는 비직관적 동작.

Lazy 가산으로 전환: reservation 진행 중에는 lab usage 변동 없음. 잡이 어떤 형태로든 종료되는 시점에 한꺼번에 가산.

잡 종료 사건가산
dispatch 성공 → 정상 종료선점 비용 + 실행 비용 (전부)
R3 강제 해제선점 비용만
Cancel (reservation 중)선점 비용만
Placement 사라짐 (R5)선점 비용만

핵심 invariant: 잡이 점유한 모든 GPU-second는 빠짐없이 lab usage로 가산됨. 가산 시점만 lazy.

이유: Eager의 자살적 자기교정을 차단하면서, fair-share의 정직함은 그대로 유지. multi-GPU 부담은 줄어들지만 0이 되진 않음(클라우드 가격 모델과 일관).

STEP 11 R3 정의 정제: internal vs external trigger

STEP 10 이후 사용자가 추가로 짚은 미묘한 부분: "1순위 잡이 reservation을 가졌다고 해서 무조건 1순위가 영구 보장되는 것은 아니다. 같은 lab의 다른 잡이 종료되면서 lab usage가 누적되면, 결국 다른 lab이 1순위가 될 수 있다."

이는 정확히 fair-share의 의도된 동작입니다. R3는 살아있어야 하지만 트리거가 정제되어야 합니다:

트리거 종류발생 조건Eager에서Lazy에서
Internal 자기 잡의 reservation 가산이 자기 lab을 끌어올림 발생 (자살) 차단
External 다른 잡의 종료/새 잡 들어옴 등 외부 변동으로 lab 순위가 바뀜 발생 그대로 발생 (정상)

R3 (개정 최종): reservation 잡이 외부 사용량 변동으로 1순위 자리에서 밀려나면 즉시 강제 해제. 그때까지의 선점 시간은 잡 종료 시점에 lab usage로 가산.

이유: external trigger는 fair-share의 본질이라 차단하면 안 됨. internal trigger는 사용자 직관과 어긋나고 불필요한 자살적 메커니즘이라 차단해야 함. Lazy가 정확히 이 둘을 분리.

STEP 12 Multi-GPU 부담 인정과 인센티브 구조

사용자가 "multi-GPU 잡이 single보다 부가 비용이 많이 쌓이면 사용자들이 single로 쪼개려 할 것 아닌가" 질문.

분석: Multi-GPU의 세 가지 추가 부담:

  1. 통신 overhead — 분산 학습은 perfect scaling 안 됨 (~10-20% 손실). 정책 무관, 학습 자체의 특성
  2. 선점 비용 — reservation 시작 후 dispatch까지의 GPU 점유 시간
  3. 실패한 reservation — R3로 강제 해제 시 그 비용은 가산되지만 dispatch는 못 받음

결정: 이 부담을 받아들임. 보조나 할인 없음.

이유:

인센티브 효과 (의도된 부분): 사용자가 쪼갤 수 있는 워크로드는 single로 쪼개는 방향으로 자연스럽게 이동. 진짜 multi가 필요한 잡(단일 GPU 메모리 초과 모델 등)만 multi를 사용. 시스템 안정성 측면에서 이득.

REJECTED OPTIONS

3. 검토했지만 채택하지 않은 옵션

이 섹션은 검토 과정에서 등장했지만 채택되지 않은 옵션들과 그 이유를 기록합니다.

3.1 Tier별 큐 / Multi-Single 분리 큐

큐를 여러 개로 나누는 모델. tier당 하나, 또는 multi/single 분리.

기각 이유: 큐 간 fair-share 조정이 새로운 차원의 매직 넘버를 요구함. 한 큐에서 starvation을 막으면 다른 큐에서 발생할 수 있음. 단순함의 정반대.

3.2 Reservation 없이 시간 임계치(C 옵션)

평소엔 head-of-line skip(B)으로 동작하다가, 멀티 GPU 잡이 일정 시간 이상 대기하면 그때부터 새 single 잡 dispatch를 차단해서 슬롯을 모으는 모델.

기각 이유: LLM 백필 덕분에 reservation의 utilization 손실이 0이라는 것을 깨달은 후 C는 의미가 사라짐. 시간 임계치 T를 정해야 하는 매직 넘버 1개 추가. 단순한 reservation이 항상 더 우월.

3.3 Eager 가산

Reservation 진행 중 매 주기 incremental하게 lab usage에 가산하는 모델. 자기교정 메커니즘이 의도.

기각 이유: 자기 잡의 reservation이 자기를 1순위에서 밀어내는 자살적 자기교정이 발생. 사용자 직관과 어긋남. 또한 multi-GPU 부담이 비선형으로 누적되어 인센티브 distortion 폭증. STEP 10 참고.

3.4 선점 비용 할인 (k < 1)

선점 비용에 곱하는 multiplier k를 1보다 작게 (예: 0.5) 두는 모델. "선점은 실제로 안 쓴 거니까 할인" 직관.

기각 이유: 매직 넘버 1개 추가, 정당화 어려움, k가 운영자의 튜닝 거리가 됨. k = 1이 가장 단순하고 정직함.

3.5 잡당 reservation 비용 cap

한 잡의 누적 선점 비용이 (예: 실행 비용의 50%)에 도달하면 더 이상 가산 안 함.

기각 이유: 매직 넘버 1개. self-correction 약화. 운영 데이터 부족 상태에서 cap 값을 정할 근거 없음. 일단 단순하게 가고, 운영 후 필요하면 추가.

3.6 LLM 양보 timeout

Reservation 충족 후 dispatch 시 LLM에 SIGTERM 보내고 30초 기다린 후 강제 종료.

기각 이유: timeout 매직 넘버 1개. LLM 양보 진행 상태를 우리 쪽에서 추적해야 함. 책임 분리 안 됨. → LLM 라우터가 자체적으로 처리하도록 책임을 넘김. 우리 쪽 코드 단순화.

3.7 Reservation 잡의 1순위 영구 보장

한 번 reservation 시작했으면 그 잡 종료까지 무조건 1순위 유지. 다른 잡이 끼어들 수 없음.

기각 이유: 사용자가 명확히 지적 — "같은 lab의 다른 잡이 completion되면서 lab usage가 누적되면 다른 lab이 1순위가 되는 건 자연스러운 fair-share 동작이고 막으면 안 된다." External trigger에 의한 R3는 살아있어야 함. STEP 11 참고.

FINAL SPEC

4. 최종 정책 spec

운영 정책 페이지에 반영된 최종 정책을 기술적으로 정밀하게 다시 정리합니다.

4.1 용어

용어정의식별자
GPU1 GPU 디바이스(node_name, gpu_index)
placement같은 노드 + 같은 GPU 모델로 묶인 GPU들의 집합(node_name, tier_id)
placement.capacityplacement의 GPU 총 개수 (정적)
placement.idle현재 비어있는 GPU 개수 (동적, LLM 점유분 포함)
fitplacement.tier.sort_order ≥ job.minimum_tier_sort_order AND placement.capacity ≥ job.num_gpus
availablefit AND placement.idle ≥ job.num_gpus
reservation1순위 멀티 GPU 잡이 placement의 일부 GPU를 선점한 상태

4.2 큐와 정렬

4.3 Dispatch 알고리즘

def dispatch_once():
    # 1. placement별 (capacity, idle) 조사
    placements = query_placements()
    for p in placements:
        p.idle -= sum(reserved_gpus for reservations on p)

    # 2. 잡 풀 정렬
    jobs = query_global_queue()
    jobs.sort(key=lambda j: (j.lab.cumulative_usage, j.queue_entered_at))

    # 3. 잡을 차례로 처리
    for i, job in enumerate(jobs):
        is_top = (i == 0)

        # (a) 즉시 dispatch 가능?
        for p in fit_placements(job):
            if p.idle >= job.num_gpus:
                dispatch(job, p)
                if job.has_reservation():
                    release_reservation(job)  # ★ swap
                p.idle -= job.num_gpus
                break
        else:
            # (b) reservation 보유면 추가 idle 흡수
            if job.has_reservation():
                p = job.reserved_placement
                absorb = min(p.idle, job.num_gpus - job.reserved_gpus)
                job.reserved_gpus += absorb
                p.idle -= absorb
                if job.reserved_gpus >= job.num_gpus:
                    dispatch(job, p)
                    release_reservation(job)
            # (c) 새 reservation 시작 (1순위 + multi-GPU만)
            elif is_top and job.num_gpus >= 2:
                p = pick_best_fit_placement(job)  # tier DESC, idle DESC
                if p and p.idle > 0:
                    start_reservation(job, p, take=p.idle)
                    p.idle = 0

4.4 Reservation 규칙 (R1~R5)

ID규칙
R1reservation은 num_gpus ≥ 2 잡에만 적용
R2reservation 시작 권한은 fair-share 1순위만
R3외부 사용량 변동으로 1순위에서 밀려나면 강제 해제. 그때까지의 선점 비용은 잡 종료 시점에 가산
R4잡당 동시 reservation 1개. placement 변경 불가 (단, swap rule(a)로 dispatch는 가능)
R5placement가 사라지면 reservation 강제 해제, 잡은 GLOBAL_QUEUE_WAIT 복귀

4.5 사용량 가산 (Lazy)

선점 비용 = (선점 GPU 수) × (선점 시간 초) × (티어 TFLOPS)
실행 비용 = (실행 GPU 수) × (실행 시간 초) × (티어 TFLOPS)
잡 종료 사건가산되는 비용
dispatch 성공 → 정상 종료선점 + 실행
R3 강제 해제선점만
Cancel (reservation 중)선점만
R5 placement 사라짐선점만

4.6 데이터 모델 변화

새 컬럼 5개 (모두 jobs 테이블):

jobs.reserved_gpus               INT              NOT NULL DEFAULT 0
jobs.reserved_node_name          TEXT             NULL
jobs.reserved_tier_id            UUID             NULL  FK gpu_tiers(id)
jobs.reservation_last_accrued_at TIMESTAMPTZ      NULL
jobs.reservation_gpu_seconds     DOUBLE PRECISION NOT NULL DEFAULT 0

새 enum 0개. 새 테이블 0개. reservation_last_accrued_at은 매 dispatch 주기마다 갱신되며, reservation_gpu_seconds에 incremental TFLOP-seconds를 누적하는 데 사용된다 (Lazy 가산 — lab.cumulative_usage에는 미반영, 잡 종료 시점에 한꺼번에 가산). 부분 인덱스 WHERE reserved_gpus > 0 추가.

4.7 검증

4.8 LLM 백필 상호작용

SCENARIOS

5. 시나리오 검증

최종 정책을 다양한 시나리오에서 검증한 결과입니다. 모두 알고리즘이 자연스럽게 처리합니다.

S1. 기본 — 1순위 multi-GPU, available한 fit placement 존재

큐: jobA (num_gpus=2, lab=BCG, cumulative=24k)  ← 1순위
placement P (4090×2): idle=2

→ 알고리즘 4-(a) 적중 → 즉시 dispatch
→ 잡A RUNNING

S2. Reservation 시작 — idle 부족

큐: jobA (num_gpus=2, BCG)  ← 1순위
placement P (4090×2): idle=1 (다른 잡이 1장 점유 중)

→ 4-(a) 미적중 (idle < num_gpus)
→ 4-(c) 발동: 1순위 + multi → reservation 시작
→ jobA.reserved_gpus = 1, reserved_node = P
→ P.idle = 0 (이후 다른 잡 매칭에서 제외)

S3. Reservation 충족

[다음 주기]
잡A reservation 1/2장 보유 중
placement P: 다른 잡이 종료되어 idle=1 회복

→ 4-(b) 발동: 같은 placement의 idle 흡수
→ jobA.reserved_gpus = 2 (충족)
→ 4-(a)로 복귀 → dispatch
→ LLM 종료 + sbatch

S4. Swap — 다른 placement에서 더 빠르게 모임

[T+0]  잡A reservation 1/2장 보유 중 (placement P)
[T+5초] placement Q에서 갑자기 2장이 한꺼번에 idle (다른 작업 동시 종료)

→ 4-(a) 발동: jobA의 fit placement 중 Q가 available
→ Q에서 즉시 dispatch
→ P의 reservation 자동 해제 (★ swap)
→ P.idle = 1 (해제된 GPU는 다음 순위 잡한테 활용 가능)

S5. R3 강제 해제 — 외부 트리거

[T+0]    잡A reservation 시작 (BCG, 1/2 선점)
         BCG.cumulative_usage = 24k
[T+5분]  잡Y (BCG 다른 사용자, RUNNING) 종료
         → record_job_usage(Y) → BCG = 32k
         → KDG (28k) < BCG → KDG가 1순위
[T+5분+ε] 다음 dispatch 주기. 잡A 2순위로 강등.
         → R3 발동: 잡A reservation 강제 해제
         → 선점 비용 24,780 BCG에 가산 (BCG = 56,780)
         → 잡A 다시 GLOBAL_QUEUE_WAIT
[T+15분] KDG의 잡들 진행/종료, 다시 BCG가 1순위
         → 잡A 다시 reservation 시작 가능 (처음부터)

S6. 노드 분산 false positive — placement로 자동 해결

큐: jobA (num_gpus=2, BCG)  ← 1순위
placement P1 (node1: 4090×2): idle=1
placement P2 (node2: 4090×2): idle=1

→ 두 placement 모두 num_gpus=2 매칭 안 됨 (idle < 2)
→ jobA에 대해 4-(a) 미적중
→ 4-(c) 발동: best fit (P1 또는 P2 중 idle DESC, 동률이면 임의)
→ reservation 시작
→ false positive 차단 (sbatch 잘못된 매칭 방지)

S7. 이종 GPU 노드 — placement 자동 분리

node X에 4090×4 + 5090×4 (가상)
→ placement P1 (X, 4090): capacity=4
→ placement P2 (X, 5090): capacity=4

큐: jobA (num_gpus=4, fit ≥ 4090)
→ P1과 P2 모두 fit (capacity=4)
→ idle 충분한 곳에서 dispatch

큐: jobB (num_gpus=8)
→ 어떤 placement도 capacity ≥ 8 만족 안 함
→ NO_FIT_PLACEMENT (제출 시 거부) — 향후 placement capacity 확장 시까지

S8. 2순위 multi-GPU 잡의 latency

큐: jobA (num_gpus=2, BCG)  ← 1순위, reservation 진행 중
   jobB (num_gpus=2, KDG)  ← 2순위
placement P (4090×2): jobA가 1장 reserve, idle=0

→ jobB는 multi이지만 1순위 아님 → reservation 권한 없음 (R2)
→ 4-(c) 미적중
→ jobB는 그냥 대기
→ jobA가 dispatch되거나 R3로 해제되어야 jobB가 1순위로 올라옴

S9. Single-GPU 잡과 reservation 잡의 공존

큐: jobA (num_gpus=2, BCG)  ← 1순위, reservation 진행 중 (P에서 1장)
   jobB (num_gpus=1, KDG)  ← 2순위
placement P (4090×2): jobA가 1장 reserve, idle=0
placement Q (3080Ti×2): idle=2

→ jobA는 P에서 reservation 진행 (Q는 jobA 입장에서 fit 아닐 수도)
→ jobB는 P, Q 둘 다 fit
→ 4-(d): jobA가 처리된 후 jobB로 진행
→ jobB에 대해 4-(a): Q에서 available → dispatch
→ Q.idle = 1 (jobB가 1장 사용)

S10. OOM escalation 후 reservation

[Attempt 1] jobA (num_gpus=2, fit ≥ 3080Ti) 3080Ti에서 RUNNING → OOM
            → escalation: minimum_tier_sort_order 상향
            → fit 조건 변경: tier ≥ 4090
            → 다시 GLOBAL_QUEUE_WAIT

[Attempt 2] 다음 주기. jobA의 fit placement는 4090만.
            → 4090 placement에 idle=1 → 4-(c) reservation 시작
            → ... 충족되면 dispatch
            (자연스럽게 작동, 별도 코드 없음)

S11. Cancel during reservation

[T+0]   잡A reservation 진행 중 (1/2 선점, 5분 경과)
[T+5분] 사용자 cancel API 호출
        → reservation 해제
        → 선점 비용 (1장 × 5분 × 82.6 ≈ 24,780) BCG에 가산
        → 잡A 상태: CANCELLED

S12. Placement 사라짐 (노드 down)

[T+0]   잡A reservation 진행 중 placement P
[T+5분] node down 감지 (Slurm sinfo) → P 사라짐
        → R5 발동: 잡A reservation 강제 해제
        → 선점 비용 가산
        → 잡A는 GLOBAL_QUEUE_WAIT 복귀
        → 다음 주기에 다른 fit placement에서 시도

S13. fit placement 0개 잡 제출

사용자 POST /v1/jobs (num_gpus=4)
→ 검증: fit placements 조회
  - 모든 placement의 capacity = 2
  - fit한 placement = 0
→ 422 NO_FIT_PLACEMENT
   "현재 인프라에 num_gpus=4를 받을 수 있는 placement가 없습니다"
→ 잡 생성 안 됨

S14. LLM 백필 위에서 reservation

placement P (4090×2): 둘 다 LLM 백필 점유 중 (idle=2 — 양보 가능)

큐: jobA (num_gpus=2)  ← 1순위
→ 4-(a): P available (idle=2)
→ 즉시 dispatch
→ 우리 쪽: LLM에 즉시 종료 명령 → sbatch
→ LLM 라우터: 이 모델 dispatch 차단 (자체 처리)
→ jobA RUNNING

S15. 동일 lab 멀티 잡 폭탄 — fair-share 자기교정

[T+0]   BCG (cumulative=24k)이 multi-GPU 잡 5건 연속 제출
        잡A1, A2, A3, A4, A5 (모두 BCG, num_gpus=2)
        → 잡A1이 1순위, dispatch (placement P)
        → 잡A2~A5는 GLOBAL_QUEUE_WAIT

[T+30분] 잡A1 정상 종료 → record_job_usage(A1)
        → BCG.cumulative_usage += 49k (대략) = 73k
        → 다른 lab과 비교: KDG=30k → KDG가 1순위
        → 잡A2는 2순위로 밀림. 대기.

[T+1시간] KDG의 잡들이 처리되며 KDG 사용량 누적
         → 다시 BCG가 1순위가 될 수도, 다른 lab이 될 수도
         → fair-share 자연 회전

→ 결과: BCG가 한 lab을 독점하지 못함. 자연스럽게 균형.

LIMITS & FUTURE

6. 한계와 향후 작업

6.1 의도된 한계

6.2 알려지지 않은 위험 — 운영 관찰 필요

6.3 향후 작업 후보

항목조건
잡당 reservation 시도 횟수 cap 실패 reservation이 같은 잡에 반복적으로 발생하는 경우
Multi-GPU aging boost 2순위 멀티 잡의 latency가 운영상 문제가 되는 경우
Cross-placement 분산 학습 (NCCL multi-node) 여러 노드에 걸친 분산 학습 수요가 명확해진 경우 (Slurm --ntasks-per-node 등 활용)
Reservation breakdown 시각화 lab usage에서 "실행 비용 vs 선점 비용" 분리 표시 (운영자/사용자 가시성)

6.4 구현 단계 (Phase 1~5)

이 정책의 코드 구현은 별도 phase로 진행됩니다:

Phase변경파일
1 DB 마이그레이션 — jobs에 컬럼 4개 추가 alembic/versions/0016_*.py, app/db/models/jobs.py
2 usage.py — num_gpus 곱 버그 fix + lazy 가산 함수 app/services/usage.py + 단위 테스트
3 scheduler.py — placement-based dispatch + reservation 흐름 app/services/scheduler.py, app/slurm/parsers.py
4 Cancel/escalation/실패 경로의 reservation 정리 app/services/scheduler.py, app/api/routes/jobs.py
5 queue_snapshot API + monitor UI에 reservation 노출 app/api/routes/platform.py, app/web/assets/console.js

6.5 변경 이력

날짜변경
2026-04-10최초 작성. STEP 1~12 정책 결정 완료. Phase 1 코드 작업 대기 중
운영 정책으로 돌아가기 콘솔 메인 페이지