ngocdang83 commited on
Commit
a9e1fba
·
verified ·
1 Parent(s): dcf3731

multi-GPU robustness: translator.py

Browse files
Files changed (1) hide show
  1. src/translator.py +109 -37
src/translator.py CHANGED
@@ -10,14 +10,20 @@ from functools import lru_cache
10
  from pathlib import Path
11
  from typing import Callable, Iterator
12
 
13
- import ctranslate2
14
  import sentencepiece as spm
15
  from huggingface_hub import snapshot_download
16
 
17
  from chunker import split_chunks
18
- from hardware import HardwareProfile, detect_hardware_profile, resolve_gpu_indices
 
 
 
 
 
19
  from token_chunker import source_token_ids, split_for_translation
20
 
 
 
21
  ROOT = Path(__file__).resolve().parent.parent
22
  MODELS_DIR = Path(os.environ.get("HACHIMIMT_MODELS_DIR", ROOT / "models"))
23
  SPECIAL_ID_TO_TOKEN = {0: "<pad>", 1: "<s>", 2: "</s>", 3: "<unk>"}
@@ -155,6 +161,47 @@ def default_ct2_compute_type(device: str) -> str:
155
  return "int8_float16" if device == "cuda" else "int8_float32"
156
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  @lru_cache(maxsize=1)
159
  def _optional_torch():
160
  try:
@@ -386,6 +433,9 @@ class HachimiTranslator:
386
  batch_type = os.environ.get("HACHIMIMT_CT2_BATCH_TYPE", "tokens").strip().lower()
387
  self._ct2_batch_type = batch_type if batch_type in {"examples", "tokens"} else "tokens"
388
  self._ct2_compute_type: str | None = None
 
 
 
389
  self._batch_size = self._profile.batch_size
390
  self._tokenize_workers = self._profile.tokenize_workers
391
  self._tokenize_pool: ThreadPoolExecutor | None = None
@@ -486,11 +536,19 @@ class HachimiTranslator:
486
  msg = f"{prefix} {config.label} · {engine} · {self.device_label()}"
487
  if backend == Backend.CT2 and self._ct2_compute_type:
488
  msg += f" · compute={self._ct2_compute_type}"
 
 
 
 
489
  msg += (
490
  f" · batch_type={self._ct2_batch_type}"
491
- f" · window={self._ct2_window_multiplier}x"
492
- f" · inter={self._ct2_inter_threads}"
493
  )
 
 
 
 
494
  if beam_size is not None:
495
  msg += f" · beam={beam_size}"
496
  return msg
@@ -505,6 +563,9 @@ class HachimiTranslator:
505
  self._tokenizer = None
506
  self._model_path = None
507
  self._ct2_compute_type = None
 
 
 
508
  if self._tokenize_pool is not None:
509
  self._tokenize_pool.shutdown(wait=False, cancel_futures=True)
510
  self._tokenize_pool = None
@@ -554,50 +615,48 @@ class HachimiTranslator:
554
 
555
  env_compute_type = os.environ.get("HACHIMIMT_COMPUTE_TYPE", "").strip()
556
  ct2_device = "cuda" if self._profile.has_cuda else "cpu"
557
- attempts = [(ct2_device, default_ct2_compute_type(ct2_device))]
558
- if not env_compute_type and ct2_device == "cuda":
559
- attempts.extend([("cuda", "int8_float32"), ("cpu", "int8_float32")])
560
- elif env_compute_type and ct2_device == "cuda":
561
- attempts.append(("cpu", "int8_float32"))
562
-
563
- # Multi-GPU: nếu có >1 GPU (vd Kaggle T4x2) → CT2 chia batch ra các GPU
564
- # (~1.69× compute đo thực). Chỉ áp cho device cuda; CPU bỏ qua device_index.
565
- try:
566
- cuda_count = ctranslate2.get_cuda_device_count()
567
- except Exception:
568
- cuda_count = 1
569
- gpu_indices = resolve_gpu_indices(
570
- cuda_count, os.environ.get("HACHIMIMT_GPU_INDICES")
571
- )
 
 
 
 
 
 
 
 
572
 
573
  translator = None
574
  last_error: Exception | None = None
575
- for device, compute_type in attempts:
576
  try:
577
- kwargs = dict(
578
  device=device,
579
  compute_type=compute_type,
580
  intra_threads=self._ct2_threads,
581
  inter_threads=self._ct2_inter_threads,
 
582
  )
583
- if device == "cuda":
584
- # LUÔN truyền device_index khi cuda — kể cả 1 GPU (vd "1" để
585
- # chọn GPU 1; nếu bỏ qua thì CT2 mặc định GPU 0 = chạy sai).
586
- if not gpu_indices:
587
- raise RuntimeError("Không có GPU CUDA khả dụng.")
588
- if len(gpu_indices) == 1:
589
- kwargs["device_index"] = gpu_indices[0] # int
590
- else:
591
- # Đa GPU: list device + inter_threads=1 = 1 REPLICA/GPU.
592
- # (CT2 inter_threads = replica MỖI device, KHÔNG phải tổng.
593
- # Benchmark T4x2: 2GPU·1replica = 1.64× & NHANH NHẤT; 2
594
- # replica/GPU = 4 worker CHẬM HƠN -17% + 4× VRAM → để =1.)
595
- kwargs["device_index"] = gpu_indices
596
- kwargs["inter_threads"] = 1
597
  translator = ctranslate2.Translator(
598
  str(model_path / config.ct2_subdir), **kwargs
599
  )
600
  self._ct2_compute_type = compute_type
 
 
 
601
  break
602
  except Exception as exc:
603
  last_error = exc
@@ -690,9 +749,17 @@ class HachimiTranslator:
690
  def _runtime_window_size(self, beam_size: int) -> int:
691
  batch_size = self._runtime_batch_size(beam_size)
692
  if self._backend == Backend.CT2:
693
- return max(batch_size, batch_size * self._ct2_window_multiplier)
694
  return batch_size
695
 
 
 
 
 
 
 
 
 
696
  def _ct2_max_batch_size(self, config: ModelConfig) -> int:
697
  if self._ct2_batch_type == "tokens":
698
  return self._batch_size * config.ct2_max_input_tokens
@@ -824,8 +891,13 @@ class HachimiTranslator:
824
 
825
  for start in range(0, total, window_size):
826
  end = min(start + window_size, total)
 
 
 
 
 
827
  batch_label = (
828
- f"window {window_size}, batch {batch_size}, {self._ct2_batch_type}"
829
  if self._backend == Backend.CT2
830
  else f"batch {batch_size}"
831
  )
 
10
  from pathlib import Path
11
  from typing import Callable, Iterator
12
 
 
13
  import sentencepiece as spm
14
  from huggingface_hub import snapshot_download
15
 
16
  from chunker import split_chunks
17
+ from hardware import (
18
+ HardwareProfile,
19
+ auto_all_gpus_by_default,
20
+ detect_hardware_profile,
21
+ resolve_gpu_indices,
22
+ )
23
  from token_chunker import source_token_ids, split_for_translation
24
 
25
+ import ctranslate2
26
+
27
  ROOT = Path(__file__).resolve().parent.parent
28
  MODELS_DIR = Path(os.environ.get("HACHIMIMT_MODELS_DIR", ROOT / "models"))
29
  SPECIAL_ID_TO_TOKEN = {0: "<pad>", 1: "<s>", 2: "</s>", 3: "<unk>"}
 
161
  return "int8_float16" if device == "cuda" else "int8_float32"
162
 
163
 
164
+ def _ct2_gpu_index_attempts(gpu_indices: list[int]) -> list[list[int]]:
165
+ """Try all requested GPUs first, then one GPU before giving up to CPU."""
166
+ if len(gpu_indices) <= 1:
167
+ return [list(gpu_indices)]
168
+ return [list(gpu_indices), [gpu_indices[0]]]
169
+
170
+
171
+ def _ct2_translator_kwargs(
172
+ *,
173
+ device: str,
174
+ compute_type: str,
175
+ intra_threads: int,
176
+ inter_threads: int,
177
+ gpu_indices: list[int] | None = None,
178
+ ) -> tuple[dict[str, object], int, str | None]:
179
+ kwargs: dict[str, object] = dict(
180
+ device=device,
181
+ compute_type=compute_type,
182
+ intra_threads=intra_threads,
183
+ inter_threads=max(1, int(inter_threads)),
184
+ )
185
+ if device != "cuda":
186
+ return kwargs, max(1, int(inter_threads)), None
187
+
188
+ if not gpu_indices:
189
+ raise RuntimeError("Không có GPU CUDA khả dụng.")
190
+
191
+ selected = list(dict.fromkeys(gpu_indices))
192
+ if len(selected) == 1:
193
+ # Luôn truyền device_index, kể cả single GPU: env "1" phải dùng GPU 1.
194
+ kwargs["device_index"] = selected[0]
195
+ else:
196
+ # CT2 inter_threads = replica trên MỖI device. Với nhiều GPU, giữ 1
197
+ # replica/GPU để tránh nhân VRAM và đã nhanh hơn trong benchmark T4x2.
198
+ kwargs["device_index"] = selected
199
+ kwargs["inter_threads"] = 1
200
+
201
+ actual_inter_threads = int(kwargs["inter_threads"])
202
+ return kwargs, len(selected) * actual_inter_threads, ",".join(str(i) for i in selected)
203
+
204
+
205
  @lru_cache(maxsize=1)
206
  def _optional_torch():
207
  try:
 
433
  batch_type = os.environ.get("HACHIMIMT_CT2_BATCH_TYPE", "tokens").strip().lower()
434
  self._ct2_batch_type = batch_type if batch_type in {"examples", "tokens"} else "tokens"
435
  self._ct2_compute_type: str | None = None
436
+ self._ct2_actual_inter_threads = self._ct2_inter_threads
437
+ self._ct2_worker_count = 1
438
+ self._ct2_device_indices_label: str | None = None
439
  self._batch_size = self._profile.batch_size
440
  self._tokenize_workers = self._profile.tokenize_workers
441
  self._tokenize_pool: ThreadPoolExecutor | None = None
 
536
  msg = f"{prefix} {config.label} · {engine} · {self.device_label()}"
537
  if backend == Backend.CT2 and self._ct2_compute_type:
538
  msg += f" · compute={self._ct2_compute_type}"
539
+ window_multiplier = self._ct2_effective_window_multiplier()
540
+ window_part = f"window={self._ct2_window_multiplier}x"
541
+ if window_multiplier != self._ct2_window_multiplier:
542
+ window_part += f"/{window_multiplier}x"
543
  msg += (
544
  f" · batch_type={self._ct2_batch_type}"
545
+ f" · {window_part}"
546
+ f" · inter={self._ct2_actual_inter_threads}"
547
  )
548
+ if self._ct2_worker_count > 1:
549
+ msg += f" · workers={self._ct2_worker_count}"
550
+ if self._ct2_device_indices_label:
551
+ msg += f" · gpu={self._ct2_device_indices_label}"
552
  if beam_size is not None:
553
  msg += f" · beam={beam_size}"
554
  return msg
 
563
  self._tokenizer = None
564
  self._model_path = None
565
  self._ct2_compute_type = None
566
+ self._ct2_actual_inter_threads = self._ct2_inter_threads
567
+ self._ct2_worker_count = 1
568
+ self._ct2_device_indices_label = None
569
  if self._tokenize_pool is not None:
570
  self._tokenize_pool.shutdown(wait=False, cancel_futures=True)
571
  self._tokenize_pool = None
 
615
 
616
  env_compute_type = os.environ.get("HACHIMIMT_COMPUTE_TYPE", "").strip()
617
  ct2_device = "cuda" if self._profile.has_cuda else "cpu"
618
+ attempts: list[tuple[str, str, list[int] | None]] = []
619
+ if ct2_device == "cuda":
620
+ try:
621
+ cuda_count = ctranslate2.get_cuda_device_count()
622
+ except Exception:
623
+ cuda_count = 0
624
+ gpu_indices = resolve_gpu_indices(
625
+ cuda_count,
626
+ os.environ.get("HACHIMIMT_GPU_INDICES"),
627
+ auto_all=auto_all_gpus_by_default(),
628
+ )
629
+ compute_types = [default_ct2_compute_type("cuda")]
630
+ if not env_compute_type and "int8_float32" not in compute_types:
631
+ compute_types.append("int8_float32")
632
+ for compute_type in compute_types:
633
+ for candidate_indices in _ct2_gpu_index_attempts(gpu_indices):
634
+ attempts.append(("cuda", compute_type, candidate_indices))
635
+ attempts.append(("cpu", "int8_float32", None))
636
+ else:
637
+ cpu_compute_type = default_ct2_compute_type("cpu")
638
+ attempts.append(("cpu", cpu_compute_type, None))
639
+ if cpu_compute_type != "int8_float32":
640
+ attempts.append(("cpu", "int8_float32", None))
641
 
642
  translator = None
643
  last_error: Exception | None = None
644
+ for device, compute_type, gpu_indices in attempts:
645
  try:
646
+ kwargs, worker_count, device_indices_label = _ct2_translator_kwargs(
647
  device=device,
648
  compute_type=compute_type,
649
  intra_threads=self._ct2_threads,
650
  inter_threads=self._ct2_inter_threads,
651
+ gpu_indices=gpu_indices,
652
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  translator = ctranslate2.Translator(
654
  str(model_path / config.ct2_subdir), **kwargs
655
  )
656
  self._ct2_compute_type = compute_type
657
+ self._ct2_actual_inter_threads = int(kwargs["inter_threads"])
658
+ self._ct2_worker_count = worker_count
659
+ self._ct2_device_indices_label = device_indices_label
660
  break
661
  except Exception as exc:
662
  last_error = exc
 
749
  def _runtime_window_size(self, beam_size: int) -> int:
750
  batch_size = self._runtime_batch_size(beam_size)
751
  if self._backend == Backend.CT2:
752
+ return max(batch_size, batch_size * self._ct2_effective_window_multiplier())
753
  return batch_size
754
 
755
+ def _ct2_effective_window_multiplier(self) -> int:
756
+ multiplier = self._ct2_window_multiplier
757
+ if self._ct2_batch_type == "tokens" and self._ct2_worker_count > 1:
758
+ # Multi-GPU needs enough queued chunks for CT2 to split into multiple
759
+ # token sub-batches; cap keeps large files from over-buffering.
760
+ multiplier *= min(self._ct2_worker_count * 2, 8)
761
+ return max(1, min(32, multiplier))
762
+
763
  def _ct2_max_batch_size(self, config: ModelConfig) -> int:
764
  if self._ct2_batch_type == "tokens":
765
  return self._batch_size * config.ct2_max_input_tokens
 
891
 
892
  for start in range(0, total, window_size):
893
  end = min(start + window_size, total)
894
+ worker_label = (
895
+ f", workers {self._ct2_worker_count}"
896
+ if self._backend == Backend.CT2 and self._ct2_worker_count > 1
897
+ else ""
898
+ )
899
  batch_label = (
900
+ f"window {window_size}, batch {batch_size}, {self._ct2_batch_type}{worker_label}"
901
  if self._backend == Backend.CT2
902
  else f"batch {batch_size}"
903
  )