feat(translation): self-host NLLB translation service (shadow deploy)#9433
feat(translation): self-host NLLB translation service (shadow deploy)#9433beastoin wants to merge 27 commits into
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>
Make benchmark script reusable across model variants: - Add --model-name flag for labeling (auto-detects from NLLB /health) - Output files named per model (benchmark_<model>.json/csv) - JSON report includes model metadata - Summary header shows actual model name - NLLB /health endpoint now returns model_dir for auto-detection - Updated docstring with multi-model comparison examples Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full benchmark report documenting methodology, results across 3 NLLB model sizes (600M, 1.3B, 3.3B) vs Google Cloud Translation V3 on WMT24 test sets. Includes interactive HTML report with bar chart visualization and Mintlify-compatible MDX for developer docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Summary
backend/nllb_translation/) with CTranslate2 INT8 on GPUTranslationService— runs NLLB alongside Google API without affecting user-facing translationsbackend/charts/nllb-translation/)gcp_nllb_translation.yml)backend/scripts/benchmark_translation.py)Architecture: Provider-Agnostic Translation
Design Principle
Omi's translation layer is built as a provider-agnostic pipeline with pluggable backends. The
TranslationServiceclass owns caching, batching, and language detection — the actual translation call is a thin integration point that can switch between providers without touching business logic.Current Provider Abstraction
Switching providers:
TRANSLATION_MODEenv var controls which provider serves user-facing translations. The cache layer sits above the provider, so switching providers doesn't invalidate or duplicate cache entries.Long-Term Provider Options
Recommended path: Shadow deploy NLLB first (this PR) → validate quality metrics → if quality acceptable, switch to
TRANSLATION_MODE=nllbfor cost elimination. If license is a concern for production, swap to MADLAD-400 (Apache-2.0) — same CTranslate2 inference code, just different model weights in the initContainer.Adding a New Provider
_translate_<provider>_batch()method toTranslationServiceTRANSLATION_MODEdispatchHOSTED_TRANSLATION_API_URLto the new service's cluster DNSTRANSLATION_MODE=shadowThe shadow comparison framework built in this PR is reusable for any future provider evaluation — it logs quality metrics (exact match ratio, length ratio, latency) without affecting user-facing results.
Deployment
GitHub Actions:
gcp_nllb_translation.yml(manualworkflow_dispatch)developmentandprodenvironmentsHelm chart:
backend/charts/nllb-translation/nodeSelector: cloud.google.com/gke-accelerator: nvidia-l4Config (env vars)
TRANSLATION_MODEgooglegoogle/shadow/nllbHOSTED_TRANSLATION_API_URLTRANSLATION_SHADOW_SAMPLE_RATE1.0TRANSLATION_SHADOW_TIMEOUT_SECONDS2.0Translation Quality Benchmark
Methodology
The benchmark script (
backend/scripts/benchmark_translation.py) compares NLLB-200-distilled-600M against Google Cloud Translation V3 using standard MT evaluation methodology:Dataset: WMT24 test sets via sacrebleu (997 sentences per pair, no authentication needed).
Metrics (in order of reliability):
wmt22-comet-da. Highest correlation with human judgment but computationally expensive.Design choices:
--dry-runvalidates all dependencies and data availability before incurring API costsResults: chrF++ Quality Relative to Google (WMT24, 997 sentences/pair)
All three NLLB model sizes benchmarked on the same WMT24 test sets against Google Cloud Translation V3.
chrF++ is the primary metric — character-level, robust for all scripts including CJK. BLEU is unreliable for CJK due to tokenization mismatch.
Key findings:
Recommendation: 1.3B offers the best quality/cost ratio — fits on a single L4 GPU, loads in seconds, and achieves 74–90% of Google quality. 3.3B is only marginally better on most languages but 2x slower.
Running (Reproducible)
NLLB Service Details
/v1/translate,/health,/ready,/metrics[lang_code] + tokens + [</s>], target prefix[</s>, lang_code]Dev Cluster E2E Test Results
NLLB deployed to
dev-omi-gkecluster, pod 1/1 Ready on L4 GPU node:en → es: "Hello world" → "Hola mundo" (891ms)en → ja: "The quick brown fox..." → 急速な茶色の狐は怠惰な犬を飛び越えて (143ms)en → zh-TW: "Good morning..." → 您好,今天天天氣如何? (112ms)fr → en(auto-detect): "Bonjour le monde" → partial (auto-detect less reliable without source)Risks
detected_language_codeReview Cycle Fixes
Round 1: GPU inference executor, top-level imports, HPA CPU target, rollout strategy, model volume, test isolation
Round 2: Case-insensitive BCP-47 lookup (
zh-TW→zho_Hant)Round 3: CTranslate2 tokenization fix (NLLB
</s>EOS/decoder-start tokens)Round 4: Benchmark script rewrite — WMT24 primary corpus (no auth needed), chrF++ primary metric
Round 5: Full benchmark run — 5 language pairs (de, zh, ja, ru, uk), Google batch size fix (30K codepoint limit)
Testing
check_module_stub_pollution.py: 0 violationshelm template: valid for both dev and prod valuesTest plan
TRANSLATION_MODE=shadowon dev backendCloses #9430
by AI for @beastoin