ci: GPU 러너 동시 실행 차단 + 티어 검증을 실제 로드로 교체
Some checks failed
Windows / verify (push) Has been cancelled
Some checks failed
Windows / verify (push) Has been cancelled
러너를 여러 프로젝트가 공유한다는 전제에서 두 가지 구멍이 있었다.
1) 동시 실행 — GPU 는 나눠 쓸 수 없다. 두 작업이 같이 VRAM 을 잡으면
둘 다 OOM 이거나, 더 나쁘게는 하나가 조용히 느려져 원인을 못 찾는다.
concurrency(같은 저장소) + 러너 capacity:1(저장소 무관, 진짜 보장)을
두 겹으로 두고, 문서에 capacity 확인을 필수 절차로 넣었다.
한 대에 러너 프로세스를 여러 개 띄우면 안 된다는 것도 명시.
2) 티어 검증이 주장보다 얕았다 — "4·5티어 검증"이라 해놓고 실제로는
tier_availability() 만 봤고, 그 함수는 torch 유무만 본다. 즉
- 4티어(Seed-X AWQ Int4)는 autoawq 가 없어도 "사용 가능"
- 5티어(Qwen3-8B)는 VRAM 11GB 가 필요한데 8GB 카드에서도 "사용 가능"
게이트가 통과인데 실제로는 못 올리는 상태를 CI 가 못 잡고 있었다.
그래서 게이트 검사는 얕은 검사임을 이름과 주석에 명시하고, 그 옆에
**진짜로 모델을 올려 번역까지 하는** 항목을 새로 뒀다. 러너 GPU 가
무엇일지 모르므로 여유 VRAM 과 autoawq 유무로 후보를 거른 뒤 올릴 수
있는 것 중 가장 무거운 티어를 고르고, 건너뛴 티어와 그 이유를 결과에
남긴다. 하나도 못 올리면 실패다 (조용히 통과하지 않는다).
게이트는 통과인데 실제 로드가 실패하면 그게 찾아야 할 버그다 — 앱이
사용자에게 "쓸 수 있다"고 하고선 못 올리는 상황. 러너가 붙으면 실제
증거를 보고 tier_availability() 를 고칠지 판단한다.
- autoawq 설치 단계 추가(4티어 전용, Windows 휠이 없을 때가 있어 선택)
- 1~3티어 CUDA 경로 번역 검사는 유지하고 unload 를 finally 로 보장
검증: pytest 189개 통과, ruff clean, 워크플로 파싱·concurrency 확인,
--gpu 를 torch 없는 환경에서 실행해 6개 중 5개가 의도대로 실패
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -273,8 +273,14 @@ def run_gpu_checks() -> None:
|
||||
raise RuntimeError("상한 적용 실패 — 게임과 같이 쓸 때 VRAM 을 못 막습니다")
|
||||
return "35% 상한 적용됨"
|
||||
|
||||
@check("torch 필요 티어(4·5)가 열리는지")
|
||||
def _tiers():
|
||||
@check("torch 필요 티어(4·5) 게이트")
|
||||
def _gate():
|
||||
"""앱이 '이 티어 쓸 수 있다'고 답하는지.
|
||||
|
||||
주의: tier_availability() 는 torch 유무만 본다. autoawq 유무나 VRAM
|
||||
용량은 보지 않으므로, 여기서 통과해도 실제로 올라간다는 뜻은 아니다.
|
||||
그건 아래 항목에서 실제로 올려서 확인한다.
|
||||
"""
|
||||
from livesub.models.manager import has_torch, tier_availability
|
||||
from livesub.models.tiers import TIERS, MTBackend
|
||||
|
||||
@@ -286,9 +292,61 @@ def run_gpu_checks() -> None:
|
||||
]
|
||||
if blocked:
|
||||
raise RuntimeError(f"막힌 티어: {', '.join(blocked)}")
|
||||
return "정밀·극한 사용 가능"
|
||||
return "정밀·극한 게이트 통과 (얕은 검사)"
|
||||
|
||||
@check("GPU 로 실제 번역")
|
||||
@check("torch 티어 실제 로드 + 번역")
|
||||
def _real_load():
|
||||
"""게이트가 아니라 **진짜로 모델을 올려서** 번역까지 해본다.
|
||||
|
||||
티어마다 요구 VRAM 이 다르고(정밀 5.6GB / 극한 11GB) 정밀 티어는
|
||||
autoawq 까지 필요하다. 러너 GPU 가 무엇일지 모르므로, 올릴 수 있는
|
||||
것 중 가장 무거운 티어를 골라서 검증한다.
|
||||
|
||||
게이트는 통과인데 여기서 실패하면 그게 바로 찾아야 할 버그다 —
|
||||
앱이 사용자에게 "쓸 수 있다"고 하고선 실제로는 못 올리는 상황이다.
|
||||
"""
|
||||
import importlib.util
|
||||
|
||||
import torch
|
||||
|
||||
from livesub.models.glossary import Glossary
|
||||
from livesub.models.tiers import MTBackend, ordered_tiers
|
||||
from livesub.models.translator import create_translator
|
||||
|
||||
free_mb = torch.cuda.mem_get_info()[0] / 1024**2
|
||||
has_awq = importlib.util.find_spec("awq") is not None
|
||||
|
||||
fits, skipped = [], []
|
||||
for t in ordered_tiers():
|
||||
if t.mt.backend is not MTBackend.TRANSFORMERS:
|
||||
continue
|
||||
if t.mt.compute_type == "int4" and not has_awq:
|
||||
skipped.append(f"{t.name}(autoawq 없음)")
|
||||
elif t.mt.vram_mb > free_mb * 0.9:
|
||||
skipped.append(f"{t.name}({t.mt.vram_mb}MB 필요)")
|
||||
else:
|
||||
fits.append(t)
|
||||
|
||||
if not fits:
|
||||
raise RuntimeError(
|
||||
f"여유 VRAM {free_mb:.0f}MB 로 올릴 수 있는 torch 티어가 없습니다. "
|
||||
f"건너뜀: {', '.join(skipped)}"
|
||||
)
|
||||
|
||||
tier = fits[-1] # 올릴 수 있는 것 중 가장 무거운 것
|
||||
translator = create_translator(tier.mt, device="cuda")
|
||||
translator.load()
|
||||
try:
|
||||
out = translator.translate("Enemy coming from the left.", "en", "ko", Glossary())
|
||||
finally:
|
||||
translator.unload()
|
||||
if not any("\uac00" <= ch <= "\ud7a3" for ch in out):
|
||||
raise RuntimeError(f"{tier.name}: 한국어가 아닙니다 — {out!r}")
|
||||
|
||||
note = f" (건너뜀: {', '.join(skipped)})" if skipped else ""
|
||||
return f"{tier.name} 로드·번역 성공 {out!r}{note}"
|
||||
|
||||
@check("GPU 로 실제 번역 (1~3티어 CUDA 경로)")
|
||||
def _translate():
|
||||
from livesub.models.glossary import Glossary
|
||||
from livesub.models.packs import build_glossary
|
||||
@@ -299,7 +357,10 @@ def run_gpu_checks() -> None:
|
||||
translator = create_translator(tier.mt, device="cuda")
|
||||
translator.load()
|
||||
glossary = build_glossary(Glossary(), ["fps-common", "pubg"])
|
||||
out = translator.translate("Enemy coming from the left.", "en", "ko", glossary)
|
||||
try:
|
||||
out = translator.translate("Enemy coming from the left.", "en", "ko", glossary)
|
||||
finally:
|
||||
translator.unload()
|
||||
if not any("\uac00" <= ch <= "\ud7a3" for ch in out):
|
||||
raise RuntimeError(f"한국어가 아닙니다: {out!r}")
|
||||
ARTIFACTS.mkdir(exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user