feat: initial project setup with LLM architecture and HF integration

- Add LLM library with GPT model implementation
- Add hf-proxy for HuggingFace integration
- Add experiments for training and generation
- Add comprehensive documentation and examples
- Configure uv workspace with proper dependencies
This commit is contained in:
Sergey Penkovsky
2025-10-04 22:40:21 +03:00
commit ec07546ea8
54 changed files with 9337 additions and 0 deletions

10
hf-proxy/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
hf-proxy/.python-version Normal file
View File

@@ -0,0 +1 @@
3.10

0
hf-proxy/README.md Normal file
View File

18
hf-proxy/pyproject.toml Normal file
View File

@@ -0,0 +1,18 @@
[project]
name = "hf-proxy"
version = "0.1.0"
description = "HuggingFace adapter for custom LLM models"
readme = "README.md"
authors = [
{ name = "Sergey Penkovsky", email = "sergey.penkovsky@gmail.com" }
]
requires-python = ">=3.10"
dependencies = [
"torch>=2.3.0",
"transformers>=4.44.0",
"datasets>=2.20.0",
]
[build-system]
requires = ["uv_build>=0.8.22,<0.9.0"]
build-backend = "uv_build"

View File

@@ -0,0 +1,44 @@
"""
HF-Proxy: Адаптер для интеграции моделей llm с HuggingFace Transformers.
Этот пакет предоставляет инструменты для:
- Конвертации кастомных LLM моделей в формат HuggingFace
- Использования моделей через стандартные интерфейсы Transformers
- Загрузки моделей в HuggingFace Hub
- Создания pipelines для генерации текста
Основные классы:
- HFAdapter: Главный адаптер для преобразования моделей
- HFGPTAdapter: Адаптер для GPT моделей
- HFUtils: Утилиты для работы с адаптером
- HFTokenizerAdapter: Адаптер для кастомных токенизаторов
"""
from .hf_adapter import HFAdapter, HFGPTAdapter
from .hf_config import HFAdapterConfig, HFPretrainedConfig
from .hf_utils import HFUtils, TokenizerWrapper, create_hf_pipeline
from .hf_tokenizer import HFTokenizerAdapter, create_hf_tokenizer, convert_to_hf_format
__version__ = "0.2.0"
__author__ = "Sergey Penkovsky"
__email__ = "sergey.penkovsky@gmail.com"
__all__ = [
# Основные классы адаптера
"HFAdapter",
"HFGPTAdapter",
# Конфигурации
"HFAdapterConfig",
"HFPretrainedConfig",
# Адаптеры токенизаторов
"HFTokenizerAdapter",
"create_hf_tokenizer",
"convert_to_hf_format",
# Утилиты
"HFUtils",
"TokenizerWrapper",
"create_hf_pipeline",
]

View File

@@ -0,0 +1,299 @@
"""
Адаптер для интеграции моделей llm с HuggingFace Transformers.
"""
import torch
import torch.nn as nn
from typing import Optional, Tuple, Union, List
from transformers import (
PreTrainedModel,
GPT2LMHeadModel,
GPT2Config,
GenerationConfig,
LogitsProcessorList,
StoppingCriteriaList
)
from transformers.modeling_outputs import CausalLMOutputWithCrossAttentions
from .hf_config import HFAdapterConfig, HFPretrainedConfig
from llm.models.gpt import GPT
class HFGPTAdapter(PreTrainedModel):
"""
Адаптер для модели GPT из библиотеки llm.
Позволяет использовать кастомные GPT модели с HuggingFace Transformers.
"""
config_class = HFPretrainedConfig
def __init__(self, config: HFPretrainedConfig, llm_model: Optional[GPT] = None):
"""
Инициализация адаптера.
Args:
config: Конфигурация HuggingFace
llm_model: Опционально, предварительно созданная модель llm
"""
super().__init__(config)
# Преобразуем HF конфигурацию в формат llm
llm_config = self._hf_to_llm_config(config)
# Создаем или используем переданную модель
if llm_model is None:
self.llm_model = GPT(llm_config)
else:
self.llm_model = llm_model
# Устанавливаем веса если они есть в конфигурации
if hasattr(config, 'state_dict') and config.state_dict is not None:
self.llm_model.load_state_dict(config.state_dict)
def _hf_to_llm_config(self, hf_config: HFPretrainedConfig) -> dict:
"""
Преобразует конфигурацию HF в формат llm.
Args:
hf_config: Конфигурация HuggingFace
Returns:
dict: Конфигурация для llm модели
"""
return {
"vocab_size": hf_config.vocab_size,
"embed_dim": hf_config.hidden_size,
"num_heads": hf_config.num_attention_heads,
"num_layers": hf_config.num_hidden_layers,
"max_position_embeddings": hf_config.max_position_embeddings,
"dropout": hf_config.hidden_dropout_prob,
}
def forward(
self,
input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
labels: Optional[torch.Tensor] = None,
past_key_values: Optional[Tuple[Tuple[torch.Tensor]]] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
**kwargs
) -> Union[Tuple, CausalLMOutputWithCrossAttentions]:
"""
Прямой проход модели.
Args:
input_ids: Входные токены [batch_size, seq_len]
attention_mask: Маска внимания [batch_size, seq_len]
labels: Метки для вычисления loss [batch_size, seq_len]
past_key_values: Кешированные ключи и значения
use_cache: Использовать кеширование
output_attentions: Возвращать веса внимания
output_hidden_states: Возвращать скрытые состояния
return_dict: Возвращать словарь вместо кортежа
Returns:
CausalLMOutputWithCrossAttentions или кортеж
"""
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Основной forward pass
logits = self.llm_model(input_ids)
loss = None
if labels is not None:
# Сдвигаем логиты и метки для языкового моделирования
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
# Вычисляем cross-entropy loss
loss_fct = nn.CrossEntropyLoss()
loss = loss_fct(
shift_logits.view(-1, shift_logits.size(-1)),
shift_labels.view(-1)
)
if not return_dict:
output = (logits,)
if loss is not None:
output = (loss,) + output
return output
return CausalLMOutputWithCrossAttentions(
loss=loss,
logits=logits,
past_key_values=None, # Наша модель пока не поддерживает кеширование
hidden_states=None,
attentions=None,
cross_attentions=None,
)
def prepare_inputs_for_generation(
self,
input_ids: torch.Tensor,
past_key_values: Optional[Tuple] = None,
**kwargs
) -> dict:
"""
Подготавливает входные данные для генерации.
Args:
input_ids: Входные токены
past_key_values: Кешированные ключи и значения
Returns:
dict: Подготовленные входные данные
"""
# Наша простая реализация пока не поддерживает past_key_values
return {"input_ids": input_ids}
def can_generate(self) -> bool:
"""Проверяет, может ли модель генерировать текст."""
return True
def generate(
self,
input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
generation_config: Optional[GenerationConfig] = None,
logits_processor: Optional[LogitsProcessorList] = None,
stopping_criteria: Optional[StoppingCriteriaList] = None,
**kwargs
) -> torch.Tensor:
"""
Генерация текста с поддержкой HuggingFace интерфейса.
Args:
input_ids: Входные токены
attention_mask: Маска внимания
generation_config: Конфигурация генерации
logits_processor: Процессоры логитов
stopping_criteria: Критерии остановки
Returns:
torch.Tensor: Сгенерированные токены
"""
# Извлекаем обязательные параметры из kwargs или используем значения по умолчанию
max_new_tokens = kwargs.pop('max_new_tokens', 50)
do_sample = kwargs.pop('do_sample', True)
# Используем встроенную генерацию llm модели
return self.llm_model.generate(
x=input_ids,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
attention_mask=attention_mask,
**kwargs
)
class HFAdapter:
"""
Основной класс адаптера для преобразования моделей llm в формат HuggingFace.
"""
@staticmethod
def from_llm_model(
llm_model: GPT,
hf_config: Optional[HFAdapterConfig] = None
) -> HFGPTAdapter:
"""
Создает адаптер из существующей llm модели.
Args:
llm_model: Обученная модель из библиотеки llm
hf_config: Конфигурация для HuggingFace
Returns:
HFGPTAdapter: Адаптированная модель
"""
if hf_config is None:
# Создаем конфигурацию из модели llm
hf_config = HFAdapterConfig.from_llm_config(llm_model.config)
# Преобразуем в PretrainedConfig
pretrained_config = HFPretrainedConfig(**hf_config.to_dict())
return HFGPTAdapter(pretrained_config, llm_model)
@staticmethod
def from_pretrained(
model_path: str,
hf_config: Optional[HFAdapterConfig] = None
) -> HFGPTAdapter:
"""
Загружает модель из чекпоинта и создает адаптер.
Args:
model_path: Путь к сохраненной модели
hf_config: Конфигурация для HuggingFace
Returns:
HFGPTAdapter: Адаптированная модель
"""
# Загружаем состояние модели
state_dict = torch.load(model_path, map_location='cpu')
# Определяем конфигурацию из состояния модели или используем переданную
if hf_config is None:
# Пытаемся определить конфигурацию из состояния модели
# Это упрощенный подход - в реальности нужно сохранять конфигурацию отдельно
vocab_size = state_dict.get('_token_embeddings._embedding.weight', torch.zeros(50257, 768)).shape[0]
embed_dim = state_dict.get('_token_embeddings._embedding.weight', torch.zeros(50257, 768)).shape[1]
hf_config = HFAdapterConfig(
vocab_size=vocab_size,
hidden_size=embed_dim,
# Остальные параметры можно установить по умолчанию
)
pretrained_config = HFPretrainedConfig(**hf_config.to_dict())
# Создаем модель llm и загружаем веса
llm_config = {
"vocab_size": hf_config.vocab_size,
"embed_dim": hf_config.hidden_size,
"num_heads": hf_config.num_attention_heads,
"num_layers": hf_config.num_hidden_layers,
"max_position_embeddings": hf_config.max_position_embeddings,
"dropout": hf_config.hidden_dropout_prob,
}
llm_model = GPT(llm_config)
llm_model.load_state_dict(state_dict)
return HFGPTAdapter(pretrained_config, llm_model)
@staticmethod
def save_pretrained(
model: HFGPTAdapter,
save_directory: str,
**kwargs
):
"""
Сохраняет адаптированную модель в формате HuggingFace.
Args:
model: Адаптированная модель
save_directory: Директория для сохранения
**kwargs: Дополнительные параметры
"""
import os
import json
# Создаем директорию если не существует
os.makedirs(save_directory, exist_ok=True)
# Сохраняем конфигурацию
config_path = os.path.join(save_directory, "config.json")
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(model.config.to_dict(), f, indent=2, ensure_ascii=False)
# Сохраняем веса модели
model_path = os.path.join(save_directory, "pytorch_model.bin")
torch.save(model.llm_model.state_dict(), model_path)
# Сохраняем токенизатор если передан
if hasattr(kwargs, 'tokenizer') and kwargs['tokenizer'] is not None:
kwargs['tokenizer'].save_pretrained(save_directory)

View File

@@ -0,0 +1,134 @@
"""
Конфигурационные классы для адаптации моделей llm к HuggingFace.
"""
from dataclasses import dataclass, field
from typing import Dict, Any, Optional
from transformers import PretrainedConfig
@dataclass
class HFAdapterConfig:
"""
Конфигурация для адаптера HuggingFace.
Параметры:
model_type: Тип модели (gpt, llama, etc.)
vocab_size: Размер словаря
hidden_size: Размер скрытого слоя
num_hidden_layers: Количество слоев
num_attention_heads: Количество голов внимания
max_position_embeddings: Максимальная длина последовательности
intermediate_size: Размер промежуточного слоя FFN
hidden_dropout_prob: Вероятность dropout
attention_probs_dropout_prob: Вероятность dropout в внимании
initializer_range: Диапазон инициализации весов
layer_norm_eps: Эпсилон для LayerNorm
use_cache: Использовать кеширование
pad_token_id: ID токена паддинга
eos_token_id: ID токена конца строки
bos_token_id: ID токена начала строки
"""
model_type: str = "gpt"
vocab_size: int = 50257
hidden_size: int = 768
num_hidden_layers: int = 12
num_attention_heads: int = 12
max_position_embeddings: int = 1024
intermediate_size: int = 3072
hidden_dropout_prob: float = 0.1
attention_probs_dropout_prob: float = 0.1
initializer_range: float = 0.02
layer_norm_eps: float = 1e-5
use_cache: bool = True
pad_token_id: int = 50256
eos_token_id: int = 50256
bos_token_id: int = 50256
# Дополнительные параметры для совместимости
architectures: list = field(default_factory=lambda: ["GPT2LMHeadModel"])
torch_dtype: str = "float32"
transformers_version: str = "4.44.0"
def to_dict(self) -> Dict[str, Any]:
"""Преобразует конфигурацию в словарь."""
return {
k: v for k, v in self.__dict__.items()
if not k.startswith('_') and not callable(v)
}
@classmethod
def from_llm_config(cls, llm_config: Dict[str, Any]) -> "HFAdapterConfig":
"""
Создает конфигурацию HF из конфигурации llm.
Args:
llm_config: Конфигурация модели из библиотеки llm
Returns:
HFAdapterConfig: Конфигурация для HuggingFace
"""
# Маппинг параметров из llm в HF формат
mapping = {
"embed_dim": "hidden_size",
"num_layers": "num_hidden_layers",
"num_heads": "num_attention_heads",
"max_position_embeddings": "max_position_embeddings",
"dropout": "hidden_dropout_prob",
"vocab_size": "vocab_size"
}
hf_config_dict = {}
for llm_key, hf_key in mapping.items():
if llm_key in llm_config:
hf_config_dict[hf_key] = llm_config[llm_key]
# Устанавливаем промежуточный размер (обычно 4x hidden_size)
if "hidden_size" in hf_config_dict:
hf_config_dict["intermediate_size"] = hf_config_dict["hidden_size"] * 4
return cls(**hf_config_dict)
class HFPretrainedConfig(PretrainedConfig):
"""
Конфигурация для предобученных моделей HuggingFace.
Наследуется от PretrainedConfig для полной совместимости.
"""
model_type = "gpt"
def __init__(
self,
vocab_size=50257,
hidden_size=768,
num_hidden_layers=12,
num_attention_heads=12,
max_position_embeddings=1024,
intermediate_size=3072,
hidden_dropout_prob=0.1,
attention_probs_dropout_prob=0.1,
initializer_range=0.02,
layer_norm_eps=1e-5,
use_cache=True,
pad_token_id=50256,
eos_token_id=50256,
bos_token_id=50256,
**kwargs
):
super().__init__(
pad_token_id=pad_token_id,
eos_token_id=eos_token_id,
bos_token_id=bos_token_id,
**kwargs
)
self.vocab_size = vocab_size
self.hidden_size = hidden_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.max_position_embeddings = max_position_embeddings
self.intermediate_size = intermediate_size
self.hidden_dropout_prob = hidden_dropout_prob
self.attention_probs_dropout_prob = attention_probs_dropout_prob
self.initializer_range = initializer_range
self.layer_norm_eps = layer_norm_eps
self.use_cache = use_cache

View File

@@ -0,0 +1,418 @@
"""
Адаптер для интеграции кастомных токенизаторов llm с HuggingFace.
"""
import json
from typing import Dict, List, Optional, Union
from llm.tokenizers import BPETokenizer, BaseTokenizer
class HFTokenizerAdapter:
"""
Упрощенный адаптер для кастомных токенизаторов llm.
Предоставляет совместимый с HuggingFace интерфейс.
"""
def __init__(self, llm_tokenizer: BaseTokenizer):
"""
Инициализация адаптера.
Args:
llm_tokenizer: Кастомный токенизатор из llm
"""
self.llm_tokenizer = llm_tokenizer
# Получаем словарь и размер
self._vocab = llm_tokenizer.get_vocab()
self.vocab_size = llm_tokenizer.get_vocab_size()
# Устанавливаем специальные токены
self.pad_token = getattr(llm_tokenizer, 'pad_token', '<pad>')
self.unk_token = getattr(llm_tokenizer, 'unk_token', '<unk>')
self.bos_token = getattr(llm_tokenizer, 'bos_token', '<bos>')
self.eos_token = getattr(llm_tokenizer, 'eos_token', '<eos>')
# Сохраняем ID специальных токенов
self.pad_token_id = getattr(llm_tokenizer, 'pad_token_id', 0)
self.unk_token_id = getattr(llm_tokenizer, 'unk_token_id', 1)
self.bos_token_id = getattr(llm_tokenizer, 'bos_token_id', 2)
self.eos_token_id = getattr(llm_tokenizer, 'eos_token_id', 3)
def __call__(self, text: str, **kwargs):
"""
Вызов токенизатора с параметрами как у HuggingFace.
Args:
text: Входной текст
**kwargs: Параметры токенизации
Returns:
dict: Словарь с токенами
"""
return_tensors = kwargs.get('return_tensors', None)
padding = kwargs.get('padding', False)
truncation = kwargs.get('truncation', False)
max_length = kwargs.get('max_length', None)
add_special_tokens = kwargs.get('add_special_tokens', True)
# Кодируем текст
input_ids = self.llm_tokenizer.encode(
text,
add_special_tokens=add_special_tokens
)
# Применяем truncation
if truncation and max_length is not None and len(input_ids) > max_length:
input_ids = input_ids[:max_length]
# Применяем padding
if padding and max_length is not None and len(input_ids) < max_length:
input_ids = input_ids + [self.pad_token_id] * (max_length - len(input_ids))
# Конвертируем в тензоры если нужно
if return_tensors == "pt":
import torch
input_ids = torch.tensor([input_ids])
return {"input_ids": input_ids}
def encode(
self,
text: str,
text_pair: Optional[str] = None,
add_special_tokens: bool = True,
padding: bool = False,
truncation: bool = False,
max_length: Optional[int] = None,
return_tensors: Optional[str] = None,
**kwargs
) -> Union[List[int], List[List[int]]]:
"""
Кодирует текст в последовательность токенов.
Args:
text: Входной текст
text_pair: Второй текст (для парных задач)
add_special_tokens: Добавлять специальные токены
padding: Добавлять паддинг
truncation: Обрезать последовательность
max_length: Максимальная длина
return_tensors: Возвращать тензоры
Returns:
Список токенов или список списков токенов
"""
# Кодируем основной текст
token_ids = self.llm_tokenizer.encode(
text,
add_special_tokens=add_special_tokens
)
# Обрабатываем text_pair если есть
if text_pair is not None:
pair_ids = self.llm_tokenizer.encode(
text_pair,
add_special_tokens=False
)
token_ids.extend(pair_ids)
# Применяем truncation
if truncation and max_length is not None and len(token_ids) > max_length:
token_ids = token_ids[:max_length]
# Применяем padding
if padding and max_length is not None and len(token_ids) < max_length:
token_ids = token_ids + [self.pad_token_id] * (max_length - len(token_ids))
# Конвертируем в тензоры если нужно
if return_tensors == "pt":
import torch
return torch.tensor([token_ids])
elif return_tensors == "np":
import numpy as np
return np.array([token_ids])
return token_ids
def decode(
self,
token_ids: Union[int, List[int], List[List[int]]],
skip_special_tokens: bool = True,
**kwargs
) -> str:
"""
Декодирует последовательность токенов в текст.
Args:
token_ids: ID токенов
skip_special_tokens: Пропускать специальные токены
Returns:
str: Декодированный текст
"""
# Обрабатываем разные форматы входных данных
if isinstance(token_ids, int):
token_ids = [token_ids]
elif isinstance(token_ids, list) and len(token_ids) > 0 and isinstance(token_ids[0], list):
# Список списков - берем первый элемент
token_ids = token_ids[0]
# Фильтруем специальные токены если нужно
if skip_special_tokens:
special_ids = {self.pad_token_id, self.unk_token_id, self.bos_token_id, self.eos_token_id}
token_ids = [tid for tid in token_ids if tid not in special_ids]
return self.llm_tokenizer.decode(token_ids)
def tokenize(self, text: str, **kwargs) -> List[str]:
"""
Токенизирует текст в список строковых токенов.
Args:
text: Входной текст
Returns:
List[str]: Список токенов
"""
return self.llm_tokenizer.tokenize(text)
def pad(
self,
encoded_inputs,
padding=True,
max_length=None,
pad_to_multiple_of=None,
return_attention_mask=None,
return_tensors=None,
verbose=True,
):
"""
Pad a list of encoded inputs.
Args:
encoded_inputs: List of encoded inputs
padding: Padding strategy
max_length: Maximum length
pad_to_multiple_of: Pad to multiple of
return_attention_mask: Return attention mask
return_tensors: Return tensors
verbose: Verbose mode
Returns:
Padded inputs
"""
# Простая реализация padding для совместимости
if isinstance(encoded_inputs, (list, tuple)) and len(encoded_inputs) > 0:
# Находим максимальную длину
max_len = 0
for item in encoded_inputs:
input_ids = item["input_ids"]
# Обрабатываем разные типы данных
if isinstance(input_ids, int):
seq_len = 1
elif hasattr(input_ids, 'shape'):
seq_len = input_ids.shape[-1] if len(input_ids.shape) > 1 else len(input_ids)
else:
seq_len = len(input_ids)
max_len = max(max_len, seq_len)
if max_length is not None:
max_len = min(max_len, max_length)
# Применяем padding
for item in encoded_inputs:
input_ids = item["input_ids"]
# Получаем текущую длину
if isinstance(input_ids, int):
current_len = 1
elif hasattr(input_ids, 'shape'):
current_len = input_ids.shape[-1] if len(input_ids.shape) > 1 else len(input_ids)
else:
current_len = len(input_ids)
if current_len < max_len:
# Дополняем pad_token_id
padding_length = max_len - current_len
# Обрабатываем разные типы данных
if isinstance(input_ids, int):
item["input_ids"] = [input_ids] + [self.pad_token_id] * padding_length
elif hasattr(input_ids, 'shape'):
import torch
padding_tensor = torch.full((padding_length,), self.pad_token_id, dtype=input_ids.dtype)
item["input_ids"] = torch.cat([input_ids, padding_tensor])
else:
item["input_ids"] = input_ids + [self.pad_token_id] * padding_length
# Добавляем attention_mask если требуется
if "attention_mask" in item:
mask = item["attention_mask"]
if isinstance(mask, int):
item["attention_mask"] = [mask] + [0] * padding_length
elif hasattr(mask, 'shape'):
padding_mask = torch.zeros(padding_length, dtype=mask.dtype)
item["attention_mask"] = torch.cat([mask, padding_mask])
else:
item["attention_mask"] = mask + [0] * padding_length
elif return_attention_mask:
if isinstance(input_ids, int):
item["attention_mask"] = [1] + [0] * padding_length
elif hasattr(input_ids, 'shape'):
attention_mask = torch.ones(current_len, dtype=torch.long)
padding_mask = torch.zeros(padding_length, dtype=torch.long)
item["attention_mask"] = torch.cat([attention_mask, padding_mask])
else:
item["attention_mask"] = [1] * current_len + [0] * padding_length
# Конвертируем в тензоры если требуется
if return_tensors == "pt":
import torch
for key in list(encoded_inputs[0].keys()):
if isinstance(encoded_inputs[0][key], list):
for i in range(len(encoded_inputs)):
encoded_inputs[i][key] = torch.tensor(encoded_inputs[i][key])
return encoded_inputs
def get_vocab(self) -> Dict[str, int]:
"""Возвращает словарь токенизатора."""
return self._vocab
def __len__(self) -> int:
"""Возвращает размер словаря."""
return self.vocab_size
def save_pretrained(self, save_directory: str, **kwargs):
"""
Сохраняет токенизатор в формате HuggingFace.
Args:
save_directory: Директория для сохранения
**kwargs: Дополнительные параметры
"""
import os
# Создаем директорию если не существует
os.makedirs(save_directory, exist_ok=True)
# Сохраняем конфигурацию токенизатора
tokenizer_config = {
"tokenizer_class": self.__class__.__name__,
"llm_tokenizer_type": self.llm_tokenizer.__class__.__name__,
"vocab_size": self.vocab_size,
"pad_token": self.pad_token,
"unk_token": self.unk_token,
"bos_token": self.bos_token,
"eos_token": self.eos_token,
"pad_token_id": self.pad_token_id,
"unk_token_id": self.unk_token_id,
"bos_token_id": self.bos_token_id,
"eos_token_id": self.eos_token_id,
}
config_path = os.path.join(save_directory, "tokenizer_config.json")
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(tokenizer_config, f, ensure_ascii=False, indent=2)
# Сохраняем словарь
vocab_path = os.path.join(save_directory, "vocab.json")
with open(vocab_path, 'w', encoding='utf-8') as f:
json.dump(self._vocab, f, ensure_ascii=False, indent=2)
print(f"✅ Токенизатор сохранен в {save_directory}")
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
"""
Загружает адаптированный токенизатор.
Args:
pretrained_model_name_or_path: Путь к сохраненному токенизатору
**kwargs: Дополнительные параметры
Returns:
HFTokenizerAdapter: Загруженный адаптер
"""
import os
# Проверяем, является ли путь директорией с файлами токенизатора
if os.path.isdir(pretrained_model_name_or_path):
# Загружаем из директории
config_path = os.path.join(pretrained_model_name_or_path, "tokenizer_config.json")
vocab_path = os.path.join(pretrained_model_name_or_path, "vocab.json")
if not os.path.exists(config_path) or not os.path.exists(vocab_path):
raise FileNotFoundError(
f"Файлы токенизатора не найдены в {pretrained_model_name_or_path}"
)
# Загружаем конфигурацию
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Определяем тип токенизатора llm
llm_tokenizer_type = config.get("llm_tokenizer_type", "BPETokenizer")
if llm_tokenizer_type == "BPETokenizer":
# Создаем BPETokenizer и загружаем словарь
llm_tokenizer = BPETokenizer()
# Загружаем словарь
with open(vocab_path, 'r', encoding='utf-8') as f:
vocab = json.load(f)
llm_tokenizer.vocab = vocab
llm_tokenizer.inverse_vocab = {v: k for k, v in vocab.items()}
llm_tokenizer.vocab_size = len(vocab)
# Устанавливаем специальные токены
llm_tokenizer.pad_token = config.get("pad_token", "<pad>")
llm_tokenizer.unk_token = config.get("unk_token", "<unk>")
llm_tokenizer.bos_token = config.get("bos_token", "<bos>")
llm_tokenizer.eos_token = config.get("eos_token", "<eos>")
llm_tokenizer.pad_token_id = config.get("pad_token_id", 0)
llm_tokenizer.unk_token_id = config.get("unk_token_id", 1)
llm_tokenizer.bos_token_id = config.get("bos_token_id", 2)
llm_tokenizer.eos_token_id = config.get("eos_token_id", 3)
return cls(llm_tokenizer, **kwargs)
else:
raise ValueError(f"Неподдерживаемый тип токенизатора: {llm_tokenizer_type}")
else:
# Пытаемся загрузить как файл llm токенизатора
try:
llm_tokenizer = BPETokenizer.load(pretrained_model_name_or_path)
return cls(llm_tokenizer, **kwargs)
except:
raise ValueError(
f"Не удалось загрузить токенизатор из {pretrained_model_name_or_path}"
)
def create_hf_tokenizer(llm_tokenizer: BaseTokenizer) -> HFTokenizerAdapter:
"""
Создает адаптер HuggingFace для кастомного токенизатора.
Args:
llm_tokenizer: Токенизатор из библиотеки llm
Returns:
HFTokenizerAdapter: Адаптированный токенизатор
"""
return HFTokenizerAdapter(llm_tokenizer)
def convert_to_hf_format(llm_tokenizer: BaseTokenizer, save_directory: str):
"""
Конвертирует кастомный токенизатор в формат HuggingFace.
Args:
llm_tokenizer: Токенизатор из llm
save_directory: Директория для сохранения
"""
adapter = create_hf_tokenizer(llm_tokenizer)
adapter.save_pretrained(save_directory)
return adapter

View File

@@ -0,0 +1,325 @@
"""
Утилиты для работы с адаптером HuggingFace.
"""
import torch
import json
from typing import Dict, Any, Optional, List
from transformers import AutoTokenizer, AutoConfig
from .hf_config import HFAdapterConfig, HFPretrainedConfig
from .hf_adapter import HFAdapter, HFGPTAdapter
class HFUtils:
"""
Утилиты для работы с HuggingFace адаптером.
"""
@staticmethod
def create_hf_config_from_llm(llm_config: Dict[str, Any]) -> HFPretrainedConfig:
"""
Создает конфигурацию HuggingFace из конфигурации llm.
Args:
llm_config: Конфигурация модели из библиотеки llm
Returns:
HFPretrainedConfig: Конфигурация для HuggingFace
"""
adapter_config = HFAdapterConfig.from_llm_config(llm_config)
return HFPretrainedConfig(**adapter_config.to_dict())
@staticmethod
def convert_to_hf_format(
llm_model,
tokenizer = None,
model_name: str = "custom-gpt"
) -> tuple:
"""
Конвертирует llm модель в формат HuggingFace.
Args:
llm_model: Модель из библиотеки llm
tokenizer: Токенизатор (HF или кастомный)
model_name: Имя модели для сохранения
Returns:
tuple: (адаптированная модель, токенизатор)
"""
# Создаем адаптер
hf_model = HFAdapter.from_llm_model(llm_model)
# Если токенизатор не передан, создаем стандартный
if tokenizer is None:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2")
# Устанавливаем специальные токены
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
elif hasattr(tokenizer, '__class__') and 'BPETokenizer' in str(tokenizer.__class__):
# Если передан наш кастомный токенизатор, создаем адаптер
from .hf_tokenizer import create_hf_tokenizer
tokenizer = create_hf_tokenizer(tokenizer)
return hf_model, tokenizer
@staticmethod
def push_to_hub(
model: HFGPTAdapter,
tokenizer,
repo_name: str,
organization: Optional[str] = None,
private: bool = False,
**kwargs
):
"""
Загружает модель в HuggingFace Hub.
Args:
model: Адаптированная модель
tokenizer: Токенизатор
repo_name: Имя репозитория
organization: Организация (опционально)
private: Приватный репозиторий
**kwargs: Дополнительные параметры
"""
try:
from huggingface_hub import HfApi, ModelCard, create_repo
# Создаем репозиторий
if organization:
repo_id = f"{organization}/{repo_name}"
else:
repo_id = repo_name
create_repo(repo_id, private=private, exist_ok=True)
# Сохраняем модель локально
import tempfile
import os
with tempfile.TemporaryDirectory() as tmp_dir:
# Сохраняем модель
HFAdapter.save_pretrained(model, tmp_dir, tokenizer=tokenizer)
# Создаем Model Card
card = ModelCard.from_template(
model_name=repo_name,
language="ru",
license="apache-2.0",
tags=["llm", "gpt", "custom"],
)
card.save(os.path.join(tmp_dir, "README.md"))
# Загружаем в Hub
api = HfApi()
api.upload_folder(
folder_path=tmp_dir,
repo_id=repo_id,
commit_message="Initial commit with custom GPT model"
)
print(f"✅ Модель успешно загружена в HuggingFace Hub: {repo_id}")
except ImportError:
raise ImportError(
"Для загрузки в HuggingFace Hub установите huggingface_hub: "
"pip install huggingface_hub"
)
@staticmethod
def load_from_hub(
repo_id: str,
**kwargs
) -> tuple:
"""
Загружает модель из HuggingFace Hub.
Args:
repo_id: ID репозитория
**kwargs: Дополнительные параметры
Returns:
tuple: (модель, токенизатор)
"""
from transformers import AutoTokenizer
# Загружаем токенизатор
tokenizer = AutoTokenizer.from_pretrained(repo_id, **kwargs)
# Загружаем конфигурацию
config = AutoConfig.from_pretrained(repo_id, **kwargs)
# Создаем модель llm на основе конфигурации
llm_config = {
"vocab_size": config.vocab_size,
"embed_dim": config.hidden_size,
"num_heads": config.num_attention_heads,
"num_layers": config.num_hidden_layers,
"max_position_embeddings": config.max_position_embeddings,
"dropout": config.hidden_dropout_prob,
}
# Загружаем модель через адаптер
model = HFAdapter.from_pretrained(
f"{repo_id}/pytorch_model.bin",
HFAdapterConfig.from_llm_config(llm_config)
)
return model, tokenizer
@staticmethod
def compare_with_hf_model(
llm_model,
hf_model_name: str = "gpt2",
test_input: str = "Hello world"
) -> Dict[str, Any]:
"""
Сравнивает llm модель с эталонной моделью из HuggingFace.
Args:
llm_model: Модель из библиотеки llm
hf_model_name: Имя модели HuggingFace для сравнения
test_input: Тестовый вход
Returns:
Dict: Результаты сравнения
"""
from transformers import AutoModelForCausalLM, AutoTokenizer
# Загружаем эталонную модель
hf_tokenizer = AutoTokenizer.from_pretrained(hf_model_name)
hf_model = AutoModelForCausalLM.from_pretrained(hf_model_name)
# Подготавливаем входные данные
inputs = hf_tokenizer(test_input, return_tensors="pt")
# Получаем логиты от обеих моделей
with torch.no_grad():
hf_logits = hf_model(**inputs).logits
llm_logits = llm_model(inputs['input_ids'])
# Сравниваем результаты
hf_probs = torch.softmax(hf_logits[0, -1], dim=-1)
llm_probs = torch.softmax(llm_logits[0, -1], dim=-1)
# Вычисляем метрики
kl_divergence = torch.nn.functional.kl_div(
torch.log(llm_probs + 1e-8),
hf_probs,
reduction='batchmean'
)
cosine_similarity = torch.nn.functional.cosine_similarity(
hf_logits.flatten(),
llm_logits.flatten(),
dim=0
)
return {
"kl_divergence": kl_divergence.item(),
"cosine_similarity": cosine_similarity.item(),
"hf_top_tokens": torch.topk(hf_probs, 5).indices.tolist(),
"llm_top_tokens": torch.topk(llm_probs, 5).indices.tolist(),
}
class TokenizerWrapper:
"""
Обертка для токенизатора с дополнительными утилитами.
"""
def __init__(self, tokenizer):
self.tokenizer = tokenizer
def encode_batch(self, texts: List[str], **kwargs) -> Dict[str, torch.Tensor]:
"""
Кодирует батч текстов.
Args:
texts: Список текстов
**kwargs: Дополнительные параметры токенизации
Returns:
Dict: Токенизированные данные
"""
return self.tokenizer(
texts,
padding=True,
truncation=True,
return_tensors="pt",
**kwargs
)
def decode_batch(self, token_ids: torch.Tensor, **kwargs) -> List[str]:
"""
Декодирует батч токенов.
Args:
token_ids: Тензор с токенами
**kwargs: Дополнительные параметры декодирования
Returns:
List[str]: Декодированные тексты
"""
if token_ids.dim() == 1:
token_ids = token_ids.unsqueeze(0)
texts = []
for i in range(token_ids.size(0)):
text = self.tokenizer.decode(
token_ids[i],
skip_special_tokens=True,
**kwargs
)
texts.append(text)
return texts
def get_vocab_size(self) -> int:
"""Возвращает размер словаря."""
return len(self.tokenizer)
def get_special_tokens(self) -> Dict[str, int]:
"""Возвращает специальные токены."""
return {
"pad_token": self.tokenizer.pad_token_id,
"eos_token": self.tokenizer.eos_token_id,
"bos_token": self.tokenizer.bos_token_id,
"unk_token": self.tokenizer.unk_token_id,
}
def create_hf_pipeline(
llm_model,
tokenizer=None,
device: str = "auto",
**kwargs
):
"""
Создает HuggingFace pipeline из llm модели.
Args:
llm_model: Модель из библиотеки llm
tokenizer: Токенизатор
device: Устройство для вычислений
**kwargs: Дополнительные параметры pipeline
Returns:
transformers.Pipeline: Готовый pipeline
"""
from transformers import pipeline
# Конвертируем модель в HF формат
hf_model, tokenizer = HFUtils.convert_to_hf_format(llm_model, tokenizer)
# Создаем pipeline
pipe = pipeline(
"text-generation",
model=hf_model,
tokenizer=tokenizer,
device=device,
**kwargs
)
return pipe

View File