feat: self-hosted NLLB-200 translation service with GPU inference#9433
Conversation
|
@beastoin Required fixes before merge: by AI for @beastoin |
CP9A — Level 1 Live Test (Backend standalone)Changed-path coverage checklist:
Evidence:
P4/P5 justification: NLLB service requires ctranslate2, sentencepiece, and CUDA GPU. These are not available in CI or on this VPS. The service is a standalone FastAPI app that will be tested during dev cluster deployment (CP9C/test plan). The shadow integration in the backend (P1-P3) is fully tested. by AI for @beastoin |
CP9B — Level 2 Live Test (Backend + App integrated)Integration scope: This PR adds shadow translation mode which is OFF by default ( Evidence:
Integration verification:
L2 limitation: The NLLB service itself is a new standalone FastAPI service that requires ctranslate2 + GPU. Full integration testing of the shadow comparison path requires dev GKE cluster deployment with GPU node pool and model weights. This is tracked in the test plan. by AI for @beastoin |
FastAPI HTTP service wrapping CTranslate2 with NLLB-200-distilled-600M. Exposes /v1/translate, /health, /ready, /metrics endpoints. BCP-47 to NLLB FLORES-200 language code mapping for 19 target languages. Closes #9430 (Phase 1: shadow deployment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CUDA 12.4 runtime base, CTranslate2 + sentencepiece + FastAPI stack. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract _translate_google_batch() helper from 3 Google API call sites. Add _schedule_shadow_compare() for fire-and-forget quality comparison against self-hosted NLLB service. Shadow mode never writes to cache and never affects returned translations. Config: TRANSLATION_MODE=google|shadow, HOSTED_TRANSLATION_API_URL, TRANSLATION_SHADOW_SAMPLE_RATE, TRANSLATION_SHADOW_TIMEOUT_SECONDS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deployment, service, HPA, ServiceMonitor templates. Dev and prod values with GPU resources (nvidia.com/gpu: 1). Mirrors parakeet chart structure for consistency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests shadow isolation (no cache writes, no error propagation), Google batch helper, shadow logging (no raw text), BCP-47 mapping. 8 pass, 3 skip (NLLB deps not in CI). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o thread pool Prepend source language token to tokenized input for accurate translation. Wrap _translate_batch in run_in_executor to avoid blocking the event loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move random and utils.executors imports out of _run_shadow_compare and _schedule_shadow_compare to comply with no in-function imports rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… for prod Add targetCPUUtilizationPercentage: 70 to prevent invalid HPA render. Swap to maxUnavailable: 1 / maxSurge: 0 for GPU-constrained rollouts. Add PVC-backed model volume mount at /models. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap to maxUnavailable: 1 / maxSurge: 0 for GPU-constrained rollouts. Add PVC-backed model volume mount at /models. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move module-scope sys.modules mutations into setUpModule() function to pass check_module_stub_pollution.py scanner. Restore state in tearDownModule(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BCP47_TO_NLLB has mixed-case keys (zh-TW, zh-Hant) but _resolve_nllb_code lowercases input. Build _BCP47_TO_NLLB_LOWER for correct resolution of Traditional Chinese and other locale-tagged codes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…handling Add tests for translate_text_by_sentence and translate_units_batch shadow scheduling. Add tests verifying httpx.TimeoutException does not propagate and logs a warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… download Replace custom node affinity with cloud.google.com/gke-accelerator: nvidia-l4 selector matching existing GPU workloads. Add nvidia.com/gpu NoSchedule toleration. Replace PVC with emptyDir + HuggingFace initContainer download for simpler model provisioning. Add initContainers support to deployment template. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NLLB-200 requires </s> as EOS token appended to source tokens and </s> as decoder start token before target language code in target_prefix. Without these, CTranslate2 produces degenerate repetitive output. Source format: [src_lang] + sp_tokens + [</s>] Target prefix: [</s>, tgt_lang] Verified on dev GKE cluster: "Hello world" → "Hola mundo" (es), correct Japanese and Traditional Chinese translations, latency 80-891ms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Manual workflow_dispatch deployment to dev/prod GKE clusters, following the same pattern as gcp_diarizer.yml — build Docker image, push to GCR, Helm upgrade with image tag, verify rollout, notify on Telegram. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169f01b to
0252cfe
Compare
…orkflow ref) 1. Shadow translation failures now call record_fallback() with bounded labels instead of bare logger.warning (AGENTS fallback telemetry rule). 2. Pin HuggingFace model revision in initContainer to prevent supply-chain drift (revision=302d78f). 3. Add ref: input.branch to workflow checkout so manual deploys build the correct branch, not the workflow ref. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@beastoin by AI for @beastoin |
When workflow_dispatch checks out a different branch, GITHUB_SHA still points to the dispatch ref. Use git rev-parse --short=7 HEAD after checkout to tag the image with the actual checked-out commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Google batch helper asserts correct args (contents, parent, mime_type, target_language_code) 2. translate_units_batch test uses correct Tuple input instead of dict 3. Shadow executor scheduling asserts submit_with_context with postprocess_executor 4. Non-200 shadow response asserts record_fallback called 5. Shadow cache isolation asserts cache_translation and _set_memory_cache never called during shadow compare Total: 16 passed, 3 skipped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NLLB quality benchmark using geni's MT methodology: - FLORES-200 devtest (1012 sentences) as reference corpus - 3 metrics: COMET (primary, wmt22-comet-da), chrF++, BLEU - Language tiers: high/medium/low resource with per-tier aggregates - Google response caching to avoid re-billing (~$20/M chars) - Paired bootstrap resampling for statistical significance - Dry-run mode for setup validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FLORES-200 requires HuggingFace authentication (gated dataset). Add sacrebleu WMT22 test set as automatic fallback — covers 5 language pairs (de, zh, ja, ru, uk) with 2037 sentences each. Also removes deprecated trust_remote_code parameter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FLORES-200 requires HuggingFace gated dataset auth, making the benchmark script fail out of the box. Switch to WMT24 test sets via sacrebleu which download on demand without authentication. Coverage: 7 language pairs (es, zh, de, ru, ja, uk, hi) with ~997 sentences each from WMT24. Adds CSV output, chrF++ as primary metric (more robust than BLEU for CJK/morphologically rich languages). Verified: dry-run passes, all 16 translation tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CP7 review caught that load_wmt_data crashed with unhandled ImportError if sacrebleu was not installed, while other metric functions handled it gracefully. Added try/except ImportError with a clean error message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Google Cloud Translation V3 has a 30,720 codepoint limit per request. With 128 sentences per batch, CJK text (ja, zh) exceeded this limit causing 400 errors. Reduced to 32 sentences per batch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@beastoin by AI for @beastoin |
Thread source_language parameter from TranslationCoordinator through TranslationService.translate_units_batch → _translate_batch → _translate_nllb_batch so the NLLB server receives source_language_code in the payload. Without it, the NLLB model skips the source token prefix, which can degrade translation quality for non-English sources. TranslationCoordinator defaults source_language="en" (the typical Deepgram STT output language in the realtime listen path). Adds 3 regression tests: - test_translate_batch_passes_source_language_to_nllb - test_nllb_batch_sends_source_language_code (verifies HTTP payload) - test_nllb_batch_omits_source_when_empty Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nator Pass the session's STT language into TranslationCoordinator at the live /v4/listen callsite so NLLB receives the correct source_language_code. Multi-language sessions pass "" (auto-detect). Change coordinator default from "en" to "" so callers that don't specify a source get auto-detect rather than a potentially wrong English assumption. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…path Remove incorrect source_language=language from TranslationCoordinator construction. In the /v4/listen path, `language` is the user's preferred output language (translation target), not the source. Passing it as source_language would make NLLB translate English→English for English-preference sessions. The coordinator now defaults to source_language="" (auto-detect), which makes NLLB skip the source token prefix — the model translates without a source language hint, which is correct when source is unknown at session start. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NLLB-200 needs source language tokens for optimal translation quality. When source_language is not provided, use langdetect to detect the source language from batch contents before sending to the NLLB server. This ensures the NLLB model receives proper source token prefixes even when the caller doesn't know the source language (e.g., the live /v4/listen path where source is unknown at session start). Skips detection for very short text (<20 chars) where langdetect is unreliable — NLLB handles these without source tokens. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
langdetect returns locale-tagged codes like zh-cn/zh-tw. Normalize to base language before checking LANGDETECT_RELIABLE_LANGUAGES so Chinese and other locale-tagged detections produce source_language_code for the NLLB server. Add zh to LANGDETECT_RELIABLE_LANGUAGES. Adds regression tests for zh-cn normalization and short-text skip. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The deploy workflow installs nllb-translation as dev-omi-nllb-translation
(pattern: {env}-omi-{service}), not dev-nllb-nllb-translation. Fix the
HOSTED_TRANSLATION_API_URL to match the actual Kubernetes service name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ator Address CP8 tester coverage gaps: - LangDetectException handling returns empty (no crash) - Unreliable/unknown language codes return empty - Malformed NLLB response (missing translations key) handled gracefully - Double fallback failure (NLLB + Google both down) raises - Prometheus metric idempotent helpers verified (counter, histogram) - Coordinator passes source_language="" by default - Coordinator passes explicit source_language when configured Total: 237 passed, 3 skipped (up from 229). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The NLLB batch handler gracefully returns [] for malformed responses
(missing translations key) via .get("translations", []) — this is
correct fail-safe behavior, not an error. Rename test from
test_nllb_batch_malformed_response_raises to
test_nllb_batch_malformed_response_returns_empty.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Architecture diagram, API docs, supported languages, environment variables, performance benchmarks, deploy instructions, and local development guide for the self-hosted NLLB-200 translation service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ode pool Add HOSTED_TRANSLATION_API_URL to prod backend-listen values so it's ready when NLLB deploys. Backend gracefully falls back to Google when NLLB is unreachable. Target dedicated nllb-translation-pool node pool for workload isolation from parakeet/engine pools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…VICE_MODELS only Remove the deprecated TRANSLATION_MODE env var path from provider resolution. Translation provider is now controlled exclusively by TRANSLATION_SERVICE_MODELS (comma-separated ordered preference, mirrors STT_SERVICE_MODELS pattern) or auto-detect based on HOSTED_TRANSLATION_API_URL presence. Remove stale 'mode' field from omi_translation_mode Info metric. Replace TRANSLATION_MODE variable with TRANSLATION_PROVIDER.value throughout. Update tests to match. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…a TRANSLATION_SERVICE_MODELS HOSTED_TRANSLATION_API_URL alone no longer switches provider to NLLB. The provider is always google unless explicitly set via TRANSLATION_SERVICE_MODELS. This makes the deploy strategy safe: 1. google — default, no change needed 2. shadow — Google primary, NLLB comparison in background 3. nllb,google — NLLB primary with Google fallback 4. nllb — NLLB only Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ero-downtime deploys Flaw 1: NLLB_BEAM_SIZE code default was 4 but prod/dev set 1. If env var fails, service runs 4x slower. Default now matches prod intent. Flaw 2: HOSTED_TRANSLATION_API_URL was set but TRANSLATION_SERVICE_MODELS was missing — provider defaulted to google and shadow never activated. Added TRANSLATION_SERVICE_MODELS=shadow to both dev and prod backend-listen values. Flag 5: Rolling update strategy had maxSurge:0/maxUnavailable:1 which kills the old pod before the new one is ready (~2-5 min model download gap). Flipped to maxSurge:1/maxUnavailable:0 for zero-downtime deploys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shadow mode (background NLLB vs Google comparison) removed per manager directive. Simplifies to two modes: google (default) or nllb (with optional google fallback via TRANSLATION_SERVICE_MODELS=nllb,google). - Remove shadow enum value, shadow client, shadow metrics, shadow scheduling/comparison methods from utils/translation.py - Remove 8 shadow test classes (~360 lines), rename test file to test_translation_nllb.py - Fix pre-existing LangDetectException constructor bug in tests - Remove shadow config from README, Grafana dashboard, Helm values - Update AGENTS.md nllb_translation description 27 tests pass, 3 skipped (NLLB deps). Net -612 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… prod config - Remove leftover callsite string arguments from 3 _translate_batch() call sites that caused TypeError after shadow removal (the old callsite param was removed but callers still passed it) - Set prod TRANSLATION_SERVICE_MODELS="" so Step 1 deploys NLLB without switching traffic (matches deploy plan) - Fix README: TRANSLATION_SERVICE_MODELS=nllb still has Google fallback on errors (honest documentation) Fixes 14+2 previously failing tests in test_translation_optimization.py and test_translation_cost_optimization.py. All 231 translation tests now pass (204 passed, 27 skipped or in separate files). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- test_nllb_batch_truncated_response_returns_partial: verifies partial results when NLLB returns fewer translations than requested - test_nllb_batch_empty_contents_returns_empty: empty input list - test_translate_batch_nllb_truncated_falls_back_to_google: truncated NLLB still succeeds at the dispatch layer Addresses tester coverage gap for malformed/truncated NLLB responses. 30 tests pass, 3 skipped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
lgtm |
…ble nllb,google in prod L2 testing against real NLLB on dev cluster revealed that langdetect can return languages (e.g. Somali 'so') not in NLLB's 22-language set, causing NLLB 400 errors. Fixed _detect_source_language to only pass source_language_code when the detected language is in NLLB's supported set. Also changed prod TRANSLATION_SERVICE_MODELS from "" to "nllb,google" after successful L2 verification with 10 test cases across 8 language pairs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
L2 Live Testing Complete — NLLB on Dev ClusterBug Found & FixedDuring L2 testing against real NLLB on dev-omi-gke, discovered that Fix ( L2 Test Results (10/10 PASS)
NLLB latencies: 27–121ms depending on batch size. All 3 entry points ( Config ChangeProd Tests
Commit
by AI for @beastoin |
Deploy Monitor — T+2h Consolidated Checkpoint (01:23–03:44 UTC Jul 12)
Traffic context: Off-peak window (01:00–04:00 UTC). Notes:
Status: PASS Next checkpoints: T+4h (05:23 UTC), T+8h (09:23 UTC), T+24h (Jul 13 01:23 UTC). by AI for @beastoin |
Deploy Monitor — T+24h Final Verdict: SUCCESSMonitoring issue: #9622 (closed)
Verdict: NLLB self-hosted translation operating as designed. Deploy monitoring complete. Follow-up: ~3,952 requests/day with by AI for @beastoin |
Summary
Adds a self-hosted translation service using Meta NLLB-200 (No Language Left Behind) as a drop-in replacement for Google Cloud Translation V3. The service runs on GPU (NVIDIA L4) via CTranslate2 and integrates into the existing realtime listen pipeline with automatic fallback to Google if NLLB is unavailable.
This follows the same self-hosting pattern used for STT (Deepgram → Parakeet) and speaker embeddings (cloud → diarizer).
What's New
NLLB Translation Microservice (
nllb_translation/)POST /v1/translate— batch translation with optional source language/metrics), health (/health), readiness (/ready)Provider Selection (
utils/translation.py)TranslationProviderenum:google,nllbTRANSLATION_SERVICE_MODELSenv var — comma-separated ordered preference list (mirrorsSTT_SERVICE_MODELSpattern)TRANSLATION_SERVICE_MODELS— the URL alone never switches providerlangdetectbefore NLLB calls, filtered to NLLB's supported language setrecord_fallback(component=other, from=nllb, to=google)Infrastructure
charts/nllb-translation/) with GPU nodeSelector, tolerations, model volume, HPA, ServiceMonitor.github/workflows/gcp_nllb_translation.yml) — manual dispatch for dev/prodscripts/benchmark_nllb_performance.py) with GPU tuning sweep resultsnllb-translation-pool(g2-standard-8, 1x L4, autoscale 0-2) on prod-omi-gkemaxSurge: 1, maxUnavailable: 0— new pod starts before old one terminatesObservability
omi_translation_requests_total,omi_translation_latency_seconds,omi_translation_chars_totalnllb_requests_total,nllb_inference_latency_seconds,nllb_tokenization_latency_secondsValueErroron module re-import in testsConfiguration
Provider is controlled exclusively by
TRANSLATION_SERVICE_MODELS— the URL alone never changes provider. Default is always Google.HOSTED_TRANSLATION_API_URLmust be set fornllbto activate — if the URL is missing,nllbis skipped and the next provider in the list is tried.Files Changed
nllb_translation/{main,Dockerfile,requirements,README}.pyutils/translation.pyutils/translation_coordinator.pyrouters/transcribe.pycharts/nllb-translation/,charts/backend-listen/.github/workflows/gcp_nllb_translation.ymlscripts/benchmark_nllb_*.pycharts/monitoring/dashboards/omi-services/omi-translation.jsonAGENTS.mdtests/unit/test_translation_nllb.py+ othersTest Evidence
Unit Tests (209 passed, 3 skipped)
L2 Live Testing (NLLB on dev cluster)
Tested TranslationService integration against real NLLB service on dev-omi-gke:
E2E Integration
record_fallbackfired → Google recovery confirmedDeploy Plan
Prod config ships with
TRANSLATION_SERVICE_MODELS=nllb,google— NLLB primary with Google fallback. L2 testing confirmed NLLB works correctly on dev cluster.Sequence
nllb,googleconfiggh workflow run gcp_nllb_translation.yml -f environment=prod -f branch=mainTRANSLATION_SERVICE_MODELS=""and redeployPost-deploy verification
kubectl -n prod-omi-backend get pods -l app.kubernetes.io/name=nllb-translation/healthand/readyfrom inside the podTRANSLATION_SERVICE_MODELS=""(instant rollback to Google only)Closes #9430
by AI for @beastoin