项目文件夹

文件
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

185 行
7.6 KiB
Python

import gc
import logging
import statistics
import time
from abc import ABC
import torch
from ludwig.api_annotations import DeveloperAPI
from ludwig.constants import MAX_BATCH_SIZE_DATASET_FRACTION, MIN_POSSIBLE_BATCH_SIZE
logger = logging.getLogger(__name__)
TOTAL_STEPS = 5
@DeveloperAPI
class BatchSizeEvaluator(ABC):
def select_best_batch_size(
self,
dataset_len: int,
max_batch_size: int | None = None,
max_trials: int = 20,
is_coordinator: bool | None = True,
global_max_sequence_length: int | None = None,
) -> int:
"""Returns optimal batch size as measured by throughput (samples / sec)."""
logger.info("Tuning batch size...")
max_batch_size = max_batch_size or dataset_len
def _is_valid_batch_size(batch_size):
# make sure that batch size is valid (e.g. less than 20% of ds size and max_batch_size)
is_smaller_than_training_set = batch_size <= MAX_BATCH_SIZE_DATASET_FRACTION * dataset_len
is_under_max_batch_size = batch_size <= max_batch_size
is_valid = is_smaller_than_training_set and is_under_max_batch_size
if not is_valid and is_coordinator:
logger.info(
f"Batch size {batch_size} is invalid, must be less than or equal to "
f"{MAX_BATCH_SIZE_DATASET_FRACTION * 100}% dataset size "
f"({int(MAX_BATCH_SIZE_DATASET_FRACTION * dataset_len)} samples "
f"of {dataset_len}) and less than or equal to max batch size {max_batch_size}"
)
return is_valid
batch_size = MIN_POSSIBLE_BATCH_SIZE
best_samples_per_sec = 0
best_batch_size = None
count = 0
while count < max_trials and _is_valid_batch_size(batch_size):
if is_coordinator:
logger.info(f"Exploring batch_size={batch_size}")
gc.collect()
try:
samples_per_sec = self.evaluate(
batch_size, total_steps=TOTAL_STEPS, global_max_sequence_length=global_max_sequence_length
)
if is_coordinator:
logger.info(f"Throughput at batch_size={batch_size}: {samples_per_sec:.5f} samples/s")
if samples_per_sec < best_samples_per_sec:
# We assume that once the throughput starts degrading, it won't go up again
if is_coordinator:
logger.info(f"Throughput decrease at batch_size={batch_size}")
break
best_samples_per_sec = samples_per_sec
best_batch_size = batch_size
count += 1
# double batch size
batch_size *= 2
except RuntimeError as e:
# PyTorch only generates Runtime errors for CUDA OOM.
gc.collect()
if "CUDA out of memory" in str(e) or isinstance(e, torch.cuda.OutOfMemoryError):
if is_coordinator:
logger.info(f"OOM at batch_size={batch_size}")
else:
# Not a CUDA error
raise
break
# Ensure that some batch size is found.
# `best_batch_size` can be None if the first batch size is invalid.
if best_batch_size is None:
if is_coordinator:
logger.info(f"Could not tune batch size, using minimum batch size of {MIN_POSSIBLE_BATCH_SIZE}")
best_batch_size = MIN_POSSIBLE_BATCH_SIZE
if is_coordinator:
logger.info(f"Selected batch_size={best_batch_size}")
return best_batch_size
def evaluate(self, batch_size: int, total_steps: int = 5, global_max_sequence_length: int | None = None) -> float:
"""Evaluates throughput of the given batch size.
Return:
Median throughput in samples / sec.
"""
durations = []
for _ in range(total_steps):
self.reset()
start_ts = time.time()
self.step(batch_size, global_max_sequence_length=global_max_sequence_length)
durations.append(time.time() - start_ts)
med_duration_s = statistics.median(durations)
if med_duration_s == 0.0:
return float("inf")
return batch_size / med_duration_s
def reset(self):
"""Called at the beginning of each evaluation step."""
def step(self, batch_size: int, global_max_sequence_length: int | None = None):
"""Called each step to evaluate the given batch size."""
raise NotImplementedError("`step` must be implemented by concrete evaluator.")
class BaseLLMBatchSizeEvaluator(BatchSizeEvaluator):
"""Base class for batch size evaluators for LLM models."""
def __init__(self, trainer):
self.trainer = trainer
self.input_feature_name, self.input_feature = list(trainer.model.input_features.items())[0]
self.output_feature_name, self.output_feature = list(trainer.model.output_features.items())[0]
# Get the length of the longest input sequence from the training data
self.input_msl = self.input_feature.input_shape[0]
if trainer.model.config_obj.input_features[0].preprocessing.max_sequence_length:
self.input_msl = trainer.model.config_obj.input_features[0].preprocessing.max_sequence_length
# Get the length of the longest output sequence from the training data
self.output_msl = self.output_feature.output_shape[0]
if trainer.model.config_obj.output_features[0].preprocessing.max_sequence_length:
self.output_msl = trainer.model.config_obj.output_features[0].preprocessing.max_sequence_length
# This is useful to create the synthetic input and target data which will be a
# random sequence of integers between 0 and vocab_size
self.vocab_size = len(trainer.model.config_obj.input_features[0].encoder.vocab)
def reset(self):
self.trainer.model.reset_metrics()
self.trainer.optimizer.zero_grad()
def step(self, batch_size: int, global_max_sequence_length: int | None = None):
if global_max_sequence_length and self.input_msl + self.output_msl > global_max_sequence_length:
# In this case, we just need to make sure that the length of the synthetic data exceeds
# max_sequence_length by at most a small amount
self.input_msl = global_max_sequence_length // 2 + 1
self.output_msl = global_max_sequence_length // 2 + 1
inputs = {
self.input_feature_name: torch.randint(0, self.vocab_size, size=(batch_size, self.input_msl))
.to(self.input_feature.input_dtype)
.to(self.trainer.device)
}
targets = {
self.output_feature_name: torch.randint(0, self.vocab_size, size=(batch_size, self.output_msl))
.to(self.output_feature.get_output_dtype())
.to(self.trainer.device)
}
self.perform_step(inputs, targets)
def perform_step(self, inputs, targets):
raise NotImplementedError("perform_step method must be implemented in subclasses")
class LLMFinetuneTrainerBatchSizeEvaluator(BaseLLMBatchSizeEvaluator):
"""Batch size evaluator for training batch size for LLM finetuning."""
def perform_step(self, inputs, targets):
self.trainer.train_step(inputs, targets)
class LLMFinetunePredictBatchSizeEvaluator(BaseLLMBatchSizeEvaluator):
"""Batch size evaluator for prediction/evaluation batch size for LLM finetuning."""
def perform_step(self, inputs, targets):
with torch.no_grad():
self.trainer.dist_model((inputs, targets))