Skip to content

feat: real-time observability for GPU transcription pipeline - #39

Merged
ojfbot merged 4 commits into
fix/pin-torch-cudnn-compatfrom
feat/transcription-observability
Apr 12, 2026
Merged

feat: real-time observability for GPU transcription pipeline#39
ojfbot merged 4 commits into
fix/pin-torch-cudnn-compatfrom
feat/transcription-observability

Conversation

@ojfbot

@ojfbot ojfbot commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add ProgressReporter to transcribe_episodes.py — writes structured progress.json at each of 8 pipeline stages with GPU metrics and segment heartbeats
  • Add preflight() to aws_runner.py — validates model cache, CUDA/cuDNN, pyannote, GPU memory, and swap before transcription starts
  • Replace 60s blind PID polling with 15s _poll_with_progress() that reads progress.json, logs stage/GPU info, and detects stalls with per-stage thresholds
  • Add --strict-diarize flag to fail hard on diarization load failures (no silent fallback)
  • Add --skip-preflight flag for known-good instances

Stacked on #38 (torch pin fix) — merge that first.

Motivation

During a transcription run on 2026-04-10, the whisper model download hung silently for 60+ minutes across 2 episodes. The old polling loop (kill -0 PID every 60s) had zero visibility into what was happening. A cuDNN mismatch also silently disabled diarization. Both issues were only discovered by manual SSH investigation.

What this changes

Remote side (transcribe_episodes.py)

Feature Detail
ProgressReporter class Writes progress.json atomically (tmp + rename) at each stage transition
8 stage names model_loadingtranscriptionintro_outro_taggingtopic_taggingdiarizationspeaker_embeddingchapter_generationwrite_outputs
Segment heartbeats Updates progress every 50 segments during transcription
GPU metrics nvidia-smi utilization/VRAM via subprocess, cached 5s
--progress-file Explicit path; defaults to {episode_dir}/progress.json for single-episode runs
--strict-diarize Raises RuntimeError if pyannote/cuDNN fails instead of silent fallback

Local side (aws_runner.py)

Feature Detail
preflight() Checks model cache (>3GB, no .incomplete), CUDA tensor alloc, pyannote import, GPU memory (>4GB), swap (>8GB)
_poll_with_progress() 15s interval, reads progress.json, logs Stage: X (detail) | GPU: Y% ZMB | N/8 stages
Stall detection Per-stage warn/kill thresholds (e.g. model_loading: 5m/10m, transcription: 10m/20m, diarization: 10m/25m)
Backward compat Falls back to PID-alive polling if progress.json unavailable

Test plan

  • Provision instance, verify preflight checks all pass (model cache ✓, CUDA ✓, pyannote ✓, GPU mem ✓, swap ✓)
  • Run transcription and verify progress.json stage tracking appears in logs every 15s
  • Verify segment heartbeat count advances during transcription stage
  • Verify GPU metrics are reported (utilization %, VRAM MB)
  • Test --strict-diarize catches diarization failures
  • Test --skip-preflight bypasses checks
  • Test backward compat: run against instance without progress.json support, verify fallback to PID polling

🤖 Generated with Claude Code

Replaces blind 60-second PID-alive polling with structured progress
tracking, pre-flight validation, and stall detection. Motivated by a
production incident where a model download hung silently for 60+ minutes
across 2 episodes before manual SSH investigation caught it.

Remote side (transcribe_episodes.py):
- ProgressReporter class writes progress.json atomically at each of 8
  stage transitions (model_loading → transcription → intro_outro_tagging
  → topic_tagging → diarization → speaker_embedding → chapter_generation
  → write_outputs) with GPU metrics and segment heartbeats every 50
  segments
- --progress-file flag for explicit path, auto-defaults to
  {episode_dir}/progress.json for single-episode runs
- --strict-diarize flag fails hard if diarization can't load instead of
  silently falling back to disabled

Local side (aws_runner.py):
- _poll_with_progress() replaces _poll_until_done(): 15s interval (was
  60s), reads progress.json via SSH, logs current stage + GPU metrics
- Per-stage stall thresholds (model_loading: 5min warn/10min kill,
  transcription: 10/20min, diarization: 10/25min)
- preflight() method runs before any transcription: verifies model cache
  (>3GB, no .incomplete files), CUDA/cuDNN tensor allocation, pyannote
  import (when --diarize), GPU memory (>4GB free), swap (>8GB)
- --strict-diarize passthrough and --skip-preflight flag
- Falls back to legacy PID polling if progress.json is unavailable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ojfbot
ojfbot force-pushed the feat/transcription-observability branch from 9d08407 to 7f3375c Compare April 11, 2026 02:03
ojfbot and others added 3 commits April 11, 2026 17:10
…ngine

Tests use __new__() to bypass __init__ when testing _load_diarization
patches. The new strict_diarize attribute must be set explicitly since
__init__ is never called.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No tests are async, but pytest-asyncio with asyncio_mode="auto" wraps
every test function, causing 9+ minute hangs on tests that use
monkeypatch + module reloading. Remove the dep and config entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
process_episode now calls _poll_with_progress, but two tests still
mocked _poll_until_done. The unmocked real method ran actual
time.sleep(15) + SSH calls, causing 7+ minute hangs in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ojfbot
ojfbot merged commit e048c57 into fix/pin-torch-cudnn-compat Apr 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant