feat: 增加日志管理

This commit is contained in:
2026-03-30 16:28:56 +08:00
parent 71887bb4bb
commit 7baa3ea0c3
9 changed files with 159 additions and 63 deletions
+5
View File
@@ -1,3 +1,4 @@
import logging
from .tokenizer import BpeTokenizer from .tokenizer import BpeTokenizer
from .text import TextNormalizer from .text import TextNormalizer
from .packing import SequencePacker from .packing import SequencePacker
@@ -5,6 +6,10 @@ from .io import IOHandler
from .processors import ProcessorFactory, BaseProcessor from .processors import ProcessorFactory, BaseProcessor
from .export import export_dataset from .export import export_dataset
from .cache import cache_jsonl from .cache import cache_jsonl
from .utils import setup_logging
# 配置项目级日志记录
setup_logging()
__all__ = [ __all__ = [
'BpeTokenizer', 'BpeTokenizer',
+23 -11
View File
@@ -1,6 +1,7 @@
"""将 JSONL 文件 tokenize 后打包存储为 HDF5""" """Tokenize JSONL files and pack them into HDF5 storage."""
import json import json
import os import os
import logging
from typing import List from typing import List
from pathlib import Path from pathlib import Path
@@ -9,8 +10,12 @@ from tqdm import tqdm
from .processors import BaseProcessor from .processors import BaseProcessor
from .packing import SequencePacker from .packing import SequencePacker
from .io import IOHandler from .io import IOHandler
from .utils import error_handler
logger = logging.getLogger(__name__)
@error_handler()
def cache_jsonl( def cache_jsonl(
files: List[str], files: List[str],
output_dir: str, output_dir: str,
@@ -20,17 +25,17 @@ def cache_jsonl(
pad_value: int = 1, pad_value: int = 1,
) -> List[str]: ) -> List[str]:
""" """
将 JSONL 文件 tokenize 后打包存储为 HDF5。 Tokenize JSONL files and pack them into HDF5 storage.
Args: Args:
files: JSONL 文件路径列表 files: List of JSONL file paths
output_dir: H5 输出目录 output_dir: H5 output directory
processor: 已初始化的 Processor 实例 processor: Initialized Processor instance
pack_size: 打包长度,<=0 表示不打包 pack_size: Packing length, <=0 means no packing
pad_value: 填充值 pad_value: Padding value
Returns: Returns:
生成的 H5 文件路径列表 List of generated H5 file paths
""" """
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
output_files: List[str] = [] output_files: List[str] = []
@@ -40,8 +45,15 @@ def cache_jsonl(
arrows = [] arrows = []
with open(file_path, "r", encoding="utf-8") as f: with open(file_path, "r", encoding="utf-8") as f:
for line in tqdm(f, desc=f"Processing {file_name}", leave=False): for line_num, line in enumerate(tqdm(f, desc=f"Processing {file_name}", leave=False), start=1):
try:
arrow = processor.process(json.loads(line)) arrow = processor.process(json.loads(line))
except json.JSONDecodeError as e:
logger.warning(f"JSON decode error in {file_path} line {line_num}: {e}. Skipping line.")
continue
except Exception as e:
logger.warning(f"Unexpected error processing line {line_num} in {file_path}: {e}. Skipping line.")
continue
if arrow is not None: if arrow is not None:
arrows.append(arrow) arrows.append(arrow)
@@ -50,7 +62,7 @@ def cache_jsonl(
output = {} output = {}
for key in processor.output_keys: for key in processor.output_keys:
if pack_size > 0: if pack_size > 0:
packer = SequencePacker(pack_size, pad_value) # 每个键独立实例 packer = SequencePacker(pack_size, pad_value) # independent instance per key
output[key] = packer.pack(package[key]) output[key] = packer.pack(package[key])
else: else:
output[key] = package[key] output[key] = package[key]
@@ -58,6 +70,6 @@ def cache_jsonl(
IOHandler.save_h5(output_dir, file_name, output) IOHandler.save_h5(output_dir, file_name, output)
h5_path = os.path.join(output_dir, f"{file_name}.h5") h5_path = os.path.join(output_dir, f"{file_name}.h5")
output_files.append(h5_path) output_files.append(h5_path)
print(f"Saved {h5_path}") logger.info(f"Saved {h5_path}")
return output_files return output_files
+24 -15
View File
@@ -1,33 +1,40 @@
""" HuggingFace Dataset 分块导出为 JSONL 文件""" """Export HuggingFace Dataset to JSONL files in chunks."""
import json import json
import os import os
from typing import Callable, Optional, List, Union import logging
from typing import Callable, Optional, List, Union, Dict, Any
from datasets import Dataset
from .utils import error_handler
logger = logging.getLogger(__name__)
@error_handler()
def export_dataset( def export_dataset(
dataset, dataset: Dataset,
output_dir: str, output_dir: str,
output_prefix: str, output_prefix: str,
*, *,
chunk_size: int = 1_000_000, chunk_size: int = 1_000_000,
max_chunks: Optional[int] = None, max_chunks: Optional[int] = None,
process_func: Optional[Callable] = None, process_func: Optional[Callable[[Dict[str, Any]], Union[Dict[str, Any], List[Dict[str, Any]]]]] = None,
column: str = "text", column: str = "text",
) -> List[str]: ) -> List[str]:
""" """
HuggingFace Dataset 分块导出为 JSONL 文件。 Export HuggingFace Dataset to JSONL files in chunks.
Args: Args:
dataset: HuggingFace Dataset 对象 dataset: HuggingFace Dataset object
output_dir: 输出目录 output_dir: Output directory
output_prefix: 输出文件名前缀,如 "chinese-c4-pretrain" output_prefix: Output file name prefix, e.g., "chinese-c4-pretrain"
chunk_size: 每个文件的最大样本数 chunk_size: Maximum number of samples per file
max_chunks: 最多处理几个 chunk(用于调试) max_chunks: Maximum number of chunks to process (for debugging)
process_func: 单条样本的转换函数 (dict) -> dict | list[dict] process_func: Single sample transformation function (dict) -> dict | list[dict]
column: 默认提取的文本列名(仅在 process_func None 时使用) column: Default text column name (only used when process_func is None)
Returns: Returns:
生成的文件路径列表 List of generated file paths
""" """
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
total = len(dataset) total = len(dataset)
@@ -41,14 +48,16 @@ def export_dataset(
chunk = dataset.select(range(start, end)) chunk = dataset.select(range(start, end))
path = os.path.join(output_dir, f"{output_prefix}_chunk_{i}.jsonl") path = os.path.join(output_dir, f"{output_prefix}_chunk_{i}.jsonl")
try:
with open(path, "w", encoding="utf-8") as f: with open(path, "w", encoding="utf-8") as f:
for example in chunk: for example in chunk:
processed = process_func(example) if process_func else {column: example[column]} processed = process_func(example) if process_func else {column: example[column]}
items = processed if isinstance(processed, list) else [processed] items = processed if isinstance(processed, list) else [processed]
for item in items: for item in items:
f.write(json.dumps(item, ensure_ascii=False) + "\n") f.write(json.dumps(item, ensure_ascii=False) + "\n")
output_files.append(path) output_files.append(path)
print(f"[{i + 1}/{lim}] Saved {path}") logger.info(f"[{i + 1}/{lim}] Saved {path}")
except (OSError, IOError) as e:
logger.error(f"Failed to write chunk {i} to {path}: {e}")
return output_files return output_files
+7 -3
View File
@@ -1,13 +1,15 @@
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Dict, List, Optional, Callable
import os import os
import h5py import h5py
import torch import torch
from torch import Tensor from torch import Tensor
from .utils import error_handler
class IOHandler: class IOHandler:
"""文件和 HDF5 读写""" """File and HDF5 read/write operations."""
@staticmethod @staticmethod
def fetch_files(directory: str) -> List[str]: def fetch_files(directory: str) -> List[str]:
@@ -18,7 +20,7 @@ class IOHandler:
] ]
@staticmethod @staticmethod
def fetch_folders(root_dir: str, filter_func=None) -> List[str]: def fetch_folders(root_dir: str, filter_func: Optional[Callable[[str], bool]] = None) -> List[str]:
folders = [] folders = []
for root, dirs, _ in os.walk(root_dir): for root, dirs, _ in os.walk(root_dir):
for dir_name in dirs: for dir_name in dirs:
@@ -28,6 +30,7 @@ class IOHandler:
return folders return folders
@staticmethod @staticmethod
@error_handler()
def save_h5(output_dir: str, file_name: str, tensor_group: Dict[str, List[Tensor]]) -> None: def save_h5(output_dir: str, file_name: str, tensor_group: Dict[str, List[Tensor]]) -> None:
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
full_path = os.path.join(output_dir, f"{file_name}.h5") full_path = os.path.join(output_dir, f"{file_name}.h5")
@@ -38,6 +41,7 @@ class IOHandler:
grp.create_dataset(f'data_{idx}', data=tensor.cpu().numpy()) grp.create_dataset(f'data_{idx}', data=tensor.cpu().numpy())
@staticmethod @staticmethod
@error_handler()
def load_h5(file_path: str, share_memory=True) -> Dict[str, List[Tensor]]: def load_h5(file_path: str, share_memory=True) -> Dict[str, List[Tensor]]:
tensor_group: Dict[str, List[Tensor]] = {} tensor_group: Dict[str, List[Tensor]] = {}
+4 -1
View File
@@ -3,12 +3,14 @@ from typing import List
import torch import torch
from torch import Tensor from torch import Tensor
from .utils import error_handler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SequencePacker: class SequencePacker:
def __init__(self, pack_size: int, pad_value: int = 0, dtype=torch.int32): def __init__(self, pack_size: int, pad_value: int = 0, dtype: torch.dtype = torch.int32):
self.pack_size = pack_size self.pack_size = pack_size
self.pad_value = pad_value self.pad_value = pad_value
self.dtype = dtype self.dtype = dtype
@@ -21,6 +23,7 @@ class SequencePacker:
) )
self._current_pos = 0 self._current_pos = 0
@error_handler()
def pack(self, sequences: List[Tensor]) -> List[Tensor]: def pack(self, sequences: List[Tensor]) -> List[Tensor]:
""" """
Pack sequences into fixed-size packages. Pack sequences into fixed-size packages.
+16 -14
View File
@@ -1,13 +1,15 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, List from typing import Dict, List, Any
import torch import torch
from torch import Tensor
from .tokenizer import BpeTokenizer
class BaseProcessor(ABC): class BaseProcessor(ABC):
"""处理器抽象基类""" """Abstract base class for processors."""
@abstractmethod @abstractmethod
def process(self, input_dict: dict) -> dict: def process(self, input_dict: Dict[str, Any]) -> Dict[str, Tensor]:
pass pass
@property @property
@@ -17,12 +19,12 @@ class BaseProcessor(ABC):
class PreTrainProcessor(BaseProcessor): class PreTrainProcessor(BaseProcessor):
"""预训练数据处理器""" """Pre-training data processor."""
def __init__(self, tokenizer): def __init__(self, tokenizer: BpeTokenizer):
self.tokenizer = tokenizer self.tokenizer = tokenizer
def process(self, input_dict: dict) -> dict: def process(self, input_dict: Dict[str, Any]) -> Dict[str, Tensor]:
segment = input_dict["text"] segment = input_dict["text"]
tokens = self.tokenizer.encode(f"{segment}<eos>") tokens = self.tokenizer.encode(f"{segment}<eos>")
return {'sequence': torch.tensor(tokens, dtype=torch.int32)} return {'sequence': torch.tensor(tokens, dtype=torch.int32)}
@@ -33,12 +35,12 @@ class PreTrainProcessor(BaseProcessor):
class SFTProcessor(BaseProcessor): class SFTProcessor(BaseProcessor):
"""监督微调数据处理器""" """Supervised fine-tuning data processor."""
def __init__(self, tokenizer): def __init__(self, tokenizer: BpeTokenizer):
self.tokenizer = tokenizer self.tokenizer = tokenizer
def process(self, input_dict: dict) -> dict: def process(self, input_dict: Dict[str, Any]) -> Dict[str, Tensor]:
query, response = input_dict["query"], input_dict["response"] query, response = input_dict["query"], input_dict["response"]
q = self.tokenizer.encode( q = self.tokenizer.encode(
f"<|im_start|>user\n{query}<|im_end|>\n<|im_start|>assistant\n" f"<|im_start|>user\n{query}<|im_end|>\n<|im_start|>assistant\n"
@@ -55,12 +57,12 @@ class SFTProcessor(BaseProcessor):
class DPOProcessor(BaseProcessor): class DPOProcessor(BaseProcessor):
"""DPO 偏好学习数据处理器""" """DPO preference learning data processor."""
def __init__(self, tokenizer): def __init__(self, tokenizer: BpeTokenizer):
self.tokenizer = tokenizer self.tokenizer = tokenizer
def process(self, input_dict: dict) -> dict: def process(self, input_dict: Dict[str, Any]) -> Dict[str, Tensor]:
query = input_dict["query"] query = input_dict["query"]
chosen_response = input_dict["chosen"] chosen_response = input_dict["chosen"]
rejected_response = input_dict["rejected"] rejected_response = input_dict["rejected"]
@@ -92,7 +94,7 @@ class DPOProcessor(BaseProcessor):
class ProcessorFactory: class ProcessorFactory:
"""处理器工厂""" """Processor factory."""
_processors = { _processors = {
"pt": PreTrainProcessor, "pt": PreTrainProcessor,
@@ -101,7 +103,7 @@ class ProcessorFactory:
} }
@classmethod @classmethod
def create(cls, processor_type: str, tokenizer) -> BaseProcessor: def create(cls, processor_type: str, tokenizer: BpeTokenizer) -> BaseProcessor:
if processor_type not in cls._processors: if processor_type not in cls._processors:
raise ValueError(f"Invalid processor type: {processor_type}") raise ValueError(f"Invalid processor type: {processor_type}")
return cls._processors[processor_type](tokenizer) return cls._processors[processor_type](tokenizer)
+3 -3
View File
@@ -1,9 +1,9 @@
import re import re
from typing import Dict from typing import Dict, Optional
class TextNormalizer: class TextNormalizer:
"""文本规范化""" """Text normalization."""
DEFAULT_REPLACEMENTS = { DEFAULT_REPLACEMENTS = {
"\\[": "$$", "\\]": "$$", "\\(": "$", "\\)": "$", "\\[": "$$", "\\]": "$$", "\\(": "$", "\\)": "$",
@@ -13,7 +13,7 @@ class TextNormalizer:
'\u00A0': ' ', '\u2026': '...' '\u00A0': ' ', '\u2026': '...'
} }
def __init__(self, custom_rules: Dict[str, str] = None): def __init__(self, custom_rules: Optional[Dict[str, str]] = None):
self.replacements = {**self.DEFAULT_REPLACEMENTS, **(custom_rules or {})} self.replacements = {**self.DEFAULT_REPLACEMENTS, **(custom_rules or {})}
self._pattern = re.compile('|'.join(re.escape(k) for k in self.replacements)) self._pattern = re.compile('|'.join(re.escape(k) for k in self.replacements))
+8 -8
View File
@@ -2,11 +2,11 @@ from tokenizers import Tokenizer, Encoding
from tokenizers import decoders, processors, normalizers, pre_tokenizers from tokenizers import decoders, processors, normalizers, pre_tokenizers
from tokenizers.models import BPE from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer from tokenizers.trainers import BpeTrainer
from typing import List, Union from typing import List, Union, Optional, Tuple, Iterator
class BpeTokenizer: class BpeTokenizer:
def __init__(self, path=None): def __init__(self, path: Optional[str] = None):
self._control_tokens = ["<bos>", "<eos>", "<pad>"] self._control_tokens = ["<bos>", "<eos>", "<pad>"]
self._special_tokens = ["<|im_start|>", "<|im_end|>"] self._special_tokens = ["<|im_start|>", "<|im_end|>"]
@@ -28,7 +28,7 @@ class BpeTokenizer:
if path is not None: if path is not None:
self._tokenizer = Tokenizer.from_file(path) self._tokenizer = Tokenizer.from_file(path)
def _prepare_trainer(self, vocab_size: int, min_freq: int, reserved_token_size: int, max_token_length=18) -> tuple: def _prepare_trainer(self, vocab_size: int, min_freq: int, reserved_token_size: int, max_token_length: int = 18) -> Tuple[BpeTrainer, int, List[str]]:
assert reserved_token_size > len(self._special_tokens) assert reserved_token_size > len(self._special_tokens)
reserved_tokens = [f"<|reserve{i:02d}|>" for i in range(reserved_token_size - len(self._special_tokens))] reserved_tokens = [f"<|reserve{i:02d}|>" for i in range(reserved_token_size - len(self._special_tokens))]
detail_vocab_size = vocab_size - (len(reserved_tokens) + len(self._special_tokens)) detail_vocab_size = vocab_size - (len(reserved_tokens) + len(self._special_tokens))
@@ -49,7 +49,7 @@ class BpeTokenizer:
return trainer, detail_vocab_size, reserved_tokens return trainer, detail_vocab_size, reserved_tokens
def train(self, files, vocab_size, min_freq, reserved_token_size=100): def train(self, files: List[str], vocab_size: int, min_freq: int, reserved_token_size: int = 100) -> None:
trainer, _, reserved_tokens = self._prepare_trainer( trainer, _, reserved_tokens = self._prepare_trainer(
vocab_size=vocab_size, vocab_size=vocab_size,
min_freq=min_freq, min_freq=min_freq,
@@ -58,7 +58,7 @@ class BpeTokenizer:
self._tokenizer.train(files=files, trainer=trainer) self._tokenizer.train(files=files, trainer=trainer)
self._tokenizer.add_special_tokens(self._special_tokens + reserved_tokens) self._tokenizer.add_special_tokens(self._special_tokens + reserved_tokens)
def train_from_iterator(self, iterator, vocab_size, min_freq, reserved_token_size=100): def train_from_iterator(self, iterator: Iterator[str], vocab_size: int, min_freq: int, reserved_token_size: int = 100) -> None:
trainer, _, reserved_tokens = self._prepare_trainer( trainer, _, reserved_tokens = self._prepare_trainer(
vocab_size=vocab_size, vocab_size=vocab_size,
min_freq=min_freq, min_freq=min_freq,
@@ -67,13 +67,13 @@ class BpeTokenizer:
self._tokenizer.train_from_iterator(iterator=iterator, trainer=trainer) self._tokenizer.train_from_iterator(iterator=iterator, trainer=trainer)
self._tokenizer.add_special_tokens(self._special_tokens + reserved_tokens) self._tokenizer.add_special_tokens(self._special_tokens + reserved_tokens)
def save(self, path): def save(self, path: str) -> None:
self._tokenizer.save(path) self._tokenizer.save(path)
def load(self, path): def load(self, path: str) -> None:
self._tokenizer = Tokenizer.from_file(path) self._tokenizer = Tokenizer.from_file(path)
def encode(self, tokens: Union[str, List[str]], out_ids: bool=True, add_special_tokens: bool=False) -> List: def encode(self, tokens: Union[str, List[str]], out_ids: bool = True, add_special_tokens: bool = False) -> Union[List[int], List[str], List[List[int]], List[List[str]]]:
if isinstance(tokens, str): if isinstance(tokens, str):
encoded: Encoding = self._tokenizer.encode(tokens, add_special_tokens=add_special_tokens) encoded: Encoding = self._tokenizer.encode(tokens, add_special_tokens=add_special_tokens)
return encoded.ids if out_ids else encoded.tokens return encoded.ids if out_ids else encoded.tokens
+61
View File
@@ -0,0 +1,61 @@
import functools
import logging
import sys
import os
from typing import Optional, Callable, Any
def error_handler(
logger: Optional[logging.Logger] = None,
reraise: bool = True,
log_level: int = logging.ERROR,
capture_keyboard_interrupt: bool = False,
):
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
try:
return func(*args, **kwargs)
except Exception as e:
if not capture_keyboard_interrupt and isinstance(e, KeyboardInterrupt):
raise
nonlocal logger
log = logger or logging.getLogger(func.__module__)
log.log(
log_level,
f"Error in {func.__name__}: {e}",
exc_info=True,
)
if reraise:
raise
return None
return wrapper
return decorator
def setup_logging(level: Optional[int] = None) -> None:
if level is None:
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
level = getattr(logging, level_name, logging.INFO)
root_logger = logging.getLogger()
if root_logger.handlers:
root_logger.setLevel(level)
return
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
formatter = logging.Formatter(
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler.setFormatter(formatter)
root_logger.setLevel(level)
root_logger.addHandler(console_handler)
logging.getLogger("h5py").setLevel(logging.WARNING)
logging.getLogger("torch").setLevel(logging.WARNING)