easy-translate / translate.py
Iker's picture
Fix find_executable_batch_size deprecation
d3132fd
raw
history blame
10.4 kB
import os
import math
import argparse
import torch
from torch.utils.data import DataLoader
from tqdm import tqdm
from transformers import (
AutoModelForSeq2SeqLM,
AutoTokenizer,
PreTrainedTokenizerBase,
DataCollatorForSeq2Seq,
)
from dataset import DatasetReader, count_lines
from accelerate import Accelerator, DistributedType, find_executable_batch_size
def get_dataloader(
accelerator: Accelerator,
filename: str,
tokenizer: PreTrainedTokenizerBase,
batch_size: int,
max_length: int,
) -> DataLoader:
dataset = DatasetReader(filename, tokenizer, max_length)
if accelerator.distributed_type == DistributedType.TPU:
data_collator = DataCollatorForSeq2Seq(
tokenizer,
padding="max_length",
max_length=max_length,
label_pad_token_id=tokenizer.pad_token_id,
return_tensors="pt",
)
else:
data_collator = DataCollatorForSeq2Seq(
tokenizer,
padding=True,
label_pad_token_id=tokenizer.pad_token_id,
# max_length=max_length, No need to set max_length here, we already truncate in the preprocess function
pad_to_multiple_of=8,
return_tensors="pt",
)
return DataLoader(
dataset,
batch_size=batch_size,
collate_fn=data_collator,
num_workers=1,
)
def main(
sentences_path: str,
output_path: str,
source_lang: str,
target_lang: str,
starting_batch_size: int,
model_name: str = "facebook/m2m100_1.2B",
cache_dir: str = None,
precision: str = "32",
max_length: int = 128,
num_beams: int = 4,
num_return_sequences: int = 1,
do_sample: bool = False,
temperature: float = 1.0,
top_k: int = 50,
top_p: float = 1.0,
):
if not os.path.exists(os.path.abspath(os.path.dirname(output_path))):
os.makedirs(os.path.abspath(os.path.dirname(output_path)))
accelerator = Accelerator(
mixed_precision=precision if precision != "32" else "no",
split_batches=False,
dispatch_batches=False,
)
print(f"Loading tokenizer {model_name}...")
tokenizer = AutoTokenizer.from_pretrained(
pretrained_model_name_or_path=model_name, cache_dir=cache_dir
)
print(f"Loading model {model_name}...")
model = AutoModelForSeq2SeqLM.from_pretrained(
pretrained_model_name_or_path=model_name, cache_dir=cache_dir
)
model.eval()
print(f"Preparing data...\n")
if precision == "32":
model = model.float()
elif precision == "fp16":
model = model.half()
elif precision == "bf16":
model = model.bfloat16()
else:
raise ValueError("Precision not supported. Supported values: 32, fp16, bf16")
tokenizer.src_lang = source_lang
try:
lang_code_to_idx = tokenizer.lang_code_to_id[target_lang]
except KeyError:
raise KeyError(
f"Language {target_lang} not found in tokenizer. Available languages: {tokenizer.lang_code_to_id.keys()}"
)
gen_kwargs = {
"max_length": max_length,
"num_beams": num_beams,
"num_return_sequences": num_return_sequences,
"do_sample": do_sample,
"temperature": temperature,
"top_k": top_k,
"top_p": top_p,
}
total_lines: int = count_lines(sentences_path)
if accelerator.is_main_process:
print(
f"** Translation **\n"
f"Input file: {sentences_path}\n"
f"Output file: {output_path}\n"
f"Source language: {source_lang}\n"
f"Target language: {target_lang}\n"
f"Starting batch size: {starting_batch_size}\n"
f"Device: {str(accelerator.device).split(':')[0]}\n"
f"Num. Devices: {accelerator.num_processes}\n"
f"Distributed_type: {accelerator.distributed_type}\n"
f"Max length: {max_length}\n"
f"Precision: {model.dtype}\n"
f"Model: {model_name}\n"
)
print("** Generation parameters **")
print("\n".join(f"{k}: {v}" for k, v in gen_kwargs.items()))
print("\n")
@find_executable_batch_size(starting_batch_size=starting_batch_size)
def inference(batch_size):
nonlocal model, tokenizer, sentences_path, max_length, output_path, lang_code_to_idx, gen_kwargs, precision
print(f"Translating with batch size {batch_size}")
data_loader = get_dataloader(
accelerator=accelerator,
filename=sentences_path,
tokenizer=tokenizer,
batch_size=batch_size,
max_length=max_length,
)
model, data_loader = accelerator.prepare(model, data_loader)
samples_seen: int = 0
with tqdm(
total=total_lines,
desc="Dataset translation",
leave=True,
ascii=True,
disable=(not accelerator.is_main_process),
) as pbar, open(output_path, "w", encoding="utf-8") as output_file:
with torch.no_grad():
for step, batch in enumerate(data_loader):
batch["input_ids"] = batch["input_ids"]
batch["attention_mask"] = batch["attention_mask"]
generated_tokens = accelerator.unwrap_model(model).generate(
**batch, forced_bos_token_id=lang_code_to_idx, **gen_kwargs
)
generated_tokens = accelerator.pad_across_processes(
generated_tokens, dim=1, pad_index=tokenizer.pad_token_id
)
generated_tokens = (
accelerator.gather(generated_tokens).cpu().numpy()
)
tgt_text = tokenizer.batch_decode(
generated_tokens, skip_special_tokens=True
)
if accelerator.is_main_process:
if (
step
== math.ceil(
math.ceil(total_lines / batch_size)
/ accelerator.num_processes
)
- 1
):
tgt_text = tgt_text[
: (total_lines * num_return_sequences) - samples_seen
]
else:
samples_seen += len(tgt_text)
print("\n".join(tgt_text), file=output_file)
pbar.update(len(tgt_text) // gen_kwargs["num_return_sequences"])
inference()
print(f"Translation done.\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the translation experiments")
parser.add_argument(
"--sentences_path",
type=str,
required=True,
help="Path to a txt file containing the sentences to translate. One sentence per line.",
)
parser.add_argument(
"--output_path",
type=str,
required=True,
help="Path to a txt file where the translated sentences will be written.",
)
parser.add_argument(
"--source_lang",
type=str,
required=True,
help="Source language id. See: supported_languages.md",
)
parser.add_argument(
"--target_lang",
type=str,
required=True,
help="Target language id. See: supported_languages.md",
)
parser.add_argument(
"--starting_batch_size",
type=int,
default=128,
help="Starting batch size, we will automatically reduce it if we find an OOM error."
"If you use multiple devices, we will divide this number by the number of devices.",
)
parser.add_argument(
"--model_name",
type=str,
default="facebook/m2m100_1.2B",
help="Path to the model to use. See: https://huggingface.co/models",
)
parser.add_argument(
"--cache_dir",
type=str,
default=None,
help="Cache directory from which to load the model, or None to not cache",
)
parser.add_argument(
"--max_length",
type=int,
default=128,
help="Maximum number of tokens in the source sentence and generated sentence. "
"Increase this value to translate longer sentences, at the cost of increasing memory usage.",
)
parser.add_argument(
"--num_beams",
type=int,
default=5,
help="Number of beams for beam search, m2m10 author recommends 5, but it might use too much memory",
)
parser.add_argument(
"--num_return_sequences",
type=int,
default=1,
help="Number of possible translation to return for each sentence (num_return_sequences<=num_beams).",
)
parser.add_argument(
"--precision",
type=str,
default="32",
choices=["bf16", "fp16", "32"],
help="Precision of the model. bf16, fp16 or 32.",
)
parser.add_argument(
"--do_sample",
action="store_true",
help="Use sampling instead of beam search.",
)
parser.add_argument(
"--temperature",
type=float,
default=0.8,
help="Temperature for sampling, value used only if do_sample is True.",
)
parser.add_argument(
"--top_k",
type=int,
default=100,
help="If do_sample is True, will sample from the top k most likely tokens.",
)
parser.add_argument(
"--top_p",
type=float,
default=0.75,
help="If do_sample is True, will sample from the top k most likely tokens.",
)
args = parser.parse_args()
main(
sentences_path=args.sentences_path,
output_path=args.output_path,
source_lang=args.source_lang,
target_lang=args.target_lang,
starting_batch_size=args.starting_batch_size,
model_name=args.model_name,
cache_dir=args.cache_dir,
max_length=args.max_length,
num_beams=args.num_beams,
num_return_sequences=args.num_return_sequences,
precision=args.precision,
do_sample=args.do_sample,
temperature=args.temperature,
top_k=args.top_k,
top_p=args.top_p,
)