fix: split 'cuda:N' into device+device_index for CTranslate2
Colab A100 run exposed a GPU-only bug: DeviceManager returns
selected_device='cuda:0' and the engine passed it verbatim to
faster-whisper, but CTranslate2 only accepts device='cuda' with a
separate device_index arg -> 'unsupported device cuda:0' on every GPU
transcription. Fix: FasterWhisperEngine._split_device() splits
'cuda:N' -> ('cuda', N) and passes device_index to WhisperModel.
Notebook: server cell now pkills stale servers (old process holds
port 8000 and 401s on new keys since KeyStore loads at startup),
sets LUKESCRIBE_API_KEY_FILE explicitly, and verifies auth with
RAW_KEY before proceeding; worker cell guards read_result() is None.
+ 4 unit tests (device split contract), 131 tests pass, ruff clean.
This commit is contained in:
@@ -28,7 +28,13 @@ def md(source: str) -> dict:
|
||||
|
||||
|
||||
def code(source: str) -> dict:
|
||||
return {"cell_type": "code", "execution_count": None, "metadata": {}, "outputs": [], "source": source}
|
||||
return {
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": source,
|
||||
}
|
||||
|
||||
|
||||
def cells() -> list[dict]:
|
||||
@@ -94,7 +100,9 @@ def cells() -> list[dict]:
|
||||
"print('작업 디렉터리 →', os.getcwd())\n"
|
||||
"!git log --oneline -1\n"
|
||||
),
|
||||
md("## 2) 시스템 의존성 + 설치\n\nffmpeg(오디오 정규화) 설치 후 venv에 luke-scribe를 설치합니다."),
|
||||
md(
|
||||
"## 2) 시스템 의존성 + 설치\n\nffmpeg(오디오 정규화) 설치 후 venv에 luke-scribe를 설치합니다."
|
||||
),
|
||||
code(
|
||||
"# 2) 시스템 패키지 (ffmpeg — ffprobe/정규화에 필수)\n"
|
||||
"!apt-get update -qq && apt-get install -y -qq ffmpeg >/dev/null\n"
|
||||
@@ -143,15 +151,11 @@ def cells() -> list[dict]:
|
||||
"CPU-only 환경에서는 `T0 / cpu / int8`이 나왔지만,\n"
|
||||
"Colab GPU(A100 80GB / T4 16GB)에서는 GPU가 감지되어야 합니다."
|
||||
),
|
||||
code(
|
||||
"# 4) detect — GPU 감지 / 능력 등급 / 정밀도 / 워커수\n"
|
||||
"!luke-scribe detect\n"
|
||||
),
|
||||
md("## 4) 단위/통합 테스트 (127개)\n\nmock 기반 테스트가 GPU 환경에서도 전부 통과하는지 확인합니다."),
|
||||
code(
|
||||
"# 5) 테스트 스위트\n"
|
||||
"!python -m pytest tests/ -q 2>&1 | tail -5\n"
|
||||
code("# 4) detect — GPU 감지 / 능력 등급 / 정밀도 / 워커수\n!luke-scribe detect\n"),
|
||||
md(
|
||||
"## 4) 단위/통합 테스트 (127개)\n\nmock 기반 테스트가 GPU 환경에서도 전부 통과하는지 확인합니다."
|
||||
),
|
||||
code("# 5) 테스트 스위트\n!python -m pytest tests/ -q 2>&1 | tail -5\n"),
|
||||
md(
|
||||
"## 5) 실전 전사 (GPU)\n\n"
|
||||
"### 5-1) 샘플 오디오 생성\n\n"
|
||||
@@ -167,7 +171,8 @@ def cells() -> list[dict]:
|
||||
code(
|
||||
"# 7) 실전 전사 — GPU 자동 감지 + 모델 다운로드 (large-v3-turbo)\n"
|
||||
"# 첫 실행 시 Hugging Face에서 모델을 다운로드합니다 (turbo ≈ 1.6GB, 1~2분)\n"
|
||||
"# CUDA 13 환경에서 CTranslate2가 GPU를 못 열면 자동으로 CPU로 폴백합니다.\n"
|
||||
"# v0.1: 'cuda:N' device를 분리해 CTranslate2 계약에 맞게 전달하도록 수정됨\n"
|
||||
"# (기존: unsupported device cuda:0). 그래도 실패하면 CPU 폴백 안내가 출력됩니다.\n"
|
||||
"import subprocess, json\n"
|
||||
"r = subprocess.run(\n"
|
||||
" ['luke-scribe', 'transcribe', 'samples/colab-ko-en.mp3', '--language', 'ko', '--device', 'auto'],\n"
|
||||
@@ -202,10 +207,23 @@ def cells() -> list[dict]:
|
||||
"print('raw 키 캡처 완료 (표시 안 함)')\n"
|
||||
),
|
||||
code(
|
||||
"# 10) 서버 기동 (in-proc 큐, 백그라운드 — Colab에서는 %%bash --bg 또는 nohup 사용)\n"
|
||||
"# 10) 서버 기동 (in-proc 큐, 백그라운드 — nohup)\n"
|
||||
"# 이전 실행에서 남은 서버가 포트 8000을 점유하면 새 키를 모른 채 401이 난다.\n"
|
||||
"# → 먼저 기존 서버를 모두 종료하고, 키 파일 경로를 명시해 재시작한다.\n"
|
||||
"import subprocess, os, time, urllib.request, urllib.error\n"
|
||||
"\n"
|
||||
"assert 'RAW_KEY' in globals(), '셀 9(키 생성)를 먼저 실행하세요'\n"
|
||||
"\n"
|
||||
"subprocess.run(['pkill', '-f', 'luke-scribe serve'], capture_output=True)\n"
|
||||
"time.sleep(2)\n"
|
||||
"\n"
|
||||
"# 서버가 읽을 키 파일/큐를 명시 (cwd 의존 제거)\n"
|
||||
"os.environ['LUKESCRIBE_API_KEY_FILE'] = '/content/api_keys.json'\n"
|
||||
"os.environ['LUKESCRIBE_QUEUE_BACKEND'] = 'inproc'\n"
|
||||
"\n"
|
||||
"!mkdir -p /content/logs\n"
|
||||
"!nohup luke-scribe serve --port 8000 > /content/logs/server.log 2>&1 &\n"
|
||||
"import time, urllib.request\n"
|
||||
"\n"
|
||||
"ok = False\n"
|
||||
"for _ in range(40):\n"
|
||||
" try:\n"
|
||||
@@ -214,9 +232,20 @@ def cells() -> list[dict]:
|
||||
" break\n"
|
||||
" except Exception:\n"
|
||||
" time.sleep(1)\n"
|
||||
"print('서버 기동 OK' if ok else '서버 기동 실패 — 로그:')\n"
|
||||
"if not ok:\n"
|
||||
" print('서버 기동 실패 — 로그:')\n"
|
||||
" print(open('/content/logs/server.log').read()[-1500:])\n"
|
||||
" raise SystemExit('서버 기동 실패')\n"
|
||||
"\n"
|
||||
"# 키 인증 검증 — RAW_KEY로 /v1/jobs 호출 → 200이어야 함 (401이면 종료)\n"
|
||||
"req = urllib.request.Request('http://localhost:8000/v1/jobs', headers={'X-API-Key': RAW_KEY})\n"
|
||||
"try:\n"
|
||||
" resp = urllib.request.urlopen(req, timeout=5)\n"
|
||||
" print('서버 기동 OK + 키 인증 OK (HTTP', resp.status, ')')\n"
|
||||
"except urllib.error.HTTPError as e:\n"
|
||||
" print('키 인증 실패 (HTTP', e.code, ') — 서버 로그:')\n"
|
||||
" print(open('/content/logs/server.log').read()[-1500:])\n"
|
||||
" raise SystemExit('키 인증 실패 — RAW_KEY 캡처/키 파일 확인')\n"
|
||||
),
|
||||
code(
|
||||
"# 11) 업로드 → poll → 결과 (RAW_KEY는 셀 9에서 캡처된 값)\n"
|
||||
@@ -229,7 +258,7 @@ def cells() -> list[dict]:
|
||||
" ['curl', '-s', '-X', 'POST', 'http://localhost:8000/v1/jobs',\n"
|
||||
" '-H', f'X-API-Key: {RAW_KEY}',\n"
|
||||
" '-F', 'file=@samples/colab-ko-en.mp3',\n"
|
||||
" '-F', 'options={\"language\":\"ko\",\"formats\":[\"json\",\"srt\"]}'],\n"
|
||||
' \'-F\', \'options={"language":"ko","formats":["json","srt"]}\'],\n'
|
||||
" capture_output=True, text=True,\n"
|
||||
")\n"
|
||||
"job = json.loads(r.stdout)\n"
|
||||
@@ -281,9 +310,16 @@ def cells() -> list[dict]:
|
||||
"worker.drain()\n"
|
||||
"\n"
|
||||
"result = store.read_result(job.id)\n"
|
||||
"print('status:', result.status)\n"
|
||||
"print('text:', result.text[:120])\n"
|
||||
"print('device:', result.execution.device, '| ct:', result.execution.compute_type, '| rtf:', result.timings.rtf)\n"
|
||||
"if result is None:\n"
|
||||
" # 전사 실패(예: GPU 초기화 실패) 시 결과가 저장되지 않음 — 상태/오류 출력\n"
|
||||
" state = broker.get(job.id)\n"
|
||||
" print('결과 없음 — job 상태:', getattr(state, 'status', 'unknown'))\n"
|
||||
" if state is not None:\n"
|
||||
" print('job 오류:', getattr(state, 'error', None) or '없음')\n"
|
||||
"else:\n"
|
||||
" print('status:', result.status)\n"
|
||||
" print('text:', result.text[:120])\n"
|
||||
" print('device:', result.execution.device, '| ct:', result.execution.compute_type, '| rtf:', result.timings.rtf)\n"
|
||||
),
|
||||
md(
|
||||
"## 7) 벤치마크 (turbo vs large-v3)\n\n"
|
||||
@@ -310,11 +346,19 @@ def cells() -> list[dict]:
|
||||
),
|
||||
md(
|
||||
"## 8) 문제 해결\n\n"
|
||||
"### CUDA 13 환경에서 `unsupported device cuda:0` (faster-whisper/CTranslate2)\n\n"
|
||||
"Colab이 CUDA 13.0(드라이버 580)으로 올라가면서, CUDA 12용으로 빌드된\n"
|
||||
"CTranslate2 wheel이 런타임 라이브러리(cuBLAS/cuDNN)를 찾지 못하는 이슈가 발생합니다.\n"
|
||||
"설치 셀 3에서 CUDA 12 런타임을 pip로 설치하고 `LD_LIBRARY_PATH`를 잡아 해결합니다.\n"
|
||||
"그래도 안 되면 CPU로 폴백: `--device cpu --compute-type int8` (느리지만 확실).\n\n"
|
||||
"### 전사 실패: `unsupported device cuda:0`\n\n"
|
||||
'**근본 원인**: v0.1 엔진이 `device="cuda:0"`(인덱스 포함)를 그대로 CTranslate2에 전달했는데,\n'
|
||||
'CTranslate2는 `device="cuda"`만 허용하고 인덱스는 별도 `device_index` 인자로 받습니다.\n'
|
||||
"→ 엔진에서 `device`/`device_index`를 분리하도록 **수정 완료** (이 브랜치 최신 커밋 포함).\n\n"
|
||||
"참고: Colab의 CUDA 13(드라이버 580)에서는 CUDA 12용 CTranslate2 wheel이\n"
|
||||
"런타임 라이브러리(cuBLAS/cuDNN)를 못 찾을 수 있어, 설치 셀 3에서\n"
|
||||
"`nvidia-cublas-cu12`/`nvidia-cudnn-cu12` + `LD_LIBRARY_PATH`를 설정합니다.\n"
|
||||
"그래도 실패하면 CPU 폴백: `--device cpu --compute-type int8` (느리지만 확실).\n\n"
|
||||
"### API 키 인증 실패 (401)\n\n"
|
||||
"1. `api_keys.json`에는 **다이제스트만** 저장되고 raw 키는 생성 시 1회만 출력됩니다\n"
|
||||
" (보안 설계). 셀 9에서 `RAW_KEY`를 캡처했는지 확인하세요 — 파일에서 키를 읽으면 401.\n"
|
||||
"2. **이전 실행에서 남은 서버**가 포트 8000을 점유하면 새 키를 모른 채 401이 납니다.\n"
|
||||
" 셀 10이 시작 시 `pkill`로 기존 서버를 종료하고 키 인증(HTTP 200)까지 검증합니다.\n\n"
|
||||
"### 모델 다운로드 실패\n\n"
|
||||
"- Hugging Face 연결 필요. 재시도: `LUKESCRIBE_MODEL_DOWNLOAD_RETRIES=3`\n"
|
||||
"- 특정 모델만: `--model large-v3-turbo` (기본) / `--model large-v3`\n"
|
||||
|
||||
Reference in New Issue
Block a user