mirror of
https://github.com/pese-git/llm-arch-research.git
synced 2026-01-24 13:32:08 +00:00
Compare commits
44 Commits
feature/gp
...
feature/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9da4c841b | ||
|
|
b1737bbce2 | ||
|
|
1aba02cab9 | ||
|
|
9794db3e18 | ||
|
|
d947b7beb3 | ||
|
|
613d784565 | ||
|
|
38c271ca3c | ||
|
|
aec3c8adb6 | ||
|
|
90eb2f4467 | ||
|
|
a3415d404a | ||
|
|
9837ea3c3d | ||
|
|
baafca0546 | ||
|
|
516f9580fb | ||
|
|
64d33783e0 | ||
|
|
6efc946027 | ||
|
|
8018efae2a | ||
|
|
0832d78acf | ||
|
|
c338556cfe | ||
|
|
3a356f5d79 | ||
|
|
923aa51e2a | ||
|
|
ba3b04cec2 | ||
|
|
e6ca8dee6f | ||
|
|
2e72dbaf07 | ||
|
|
dc440a3938 | ||
|
|
50d7593023 | ||
|
|
38682e8c9d | ||
|
|
e791f7cd93 | ||
|
|
d10044e4a7 | ||
|
|
ec0d2bd8d0 | ||
|
|
e5706a690d | ||
|
|
3e4815fcc6 | ||
|
|
0cc7850848 | ||
|
|
237b86421e | ||
|
|
712278e33c | ||
|
|
332cad6159 | ||
|
|
2434d34188 | ||
|
|
73ee3e16ec | ||
|
|
3bc2848cf0 | ||
|
|
d99d605b35 | ||
|
|
211adf574c | ||
|
|
f30cd530a9 | ||
|
|
9898e8ee83 | ||
|
|
b6f56a2640 | ||
|
|
e5b5a97811 |
94
README.md
94
README.md
@@ -1,15 +1,16 @@
|
||||
# LLM Architecture Research
|
||||
|
||||
Исследовательский проект для разработки и обучения архитектур больших языковых моделей (LLM).
|
||||
Исследовательский проект по разработке, обучению и сравнительному анализу современных архитектур больших языковых моделей (LLM): **GPT, GPT-2, LLaMA, Mistral**. Прямая поддержка интеграции с HuggingFace (через модуль `hf-proxy`).
|
||||
|
||||
|
||||
## 🏗️ Архитектура проекта
|
||||
|
||||
Проект организован как монорепозиторий с использованием **uv** workspace:
|
||||
|
||||
- **`llm`** — основная библиотека с реализацией архитектур LLM (GPT, GPT-2)
|
||||
- **`hf-proxy`** — адаптер для интеграции с HuggingFace
|
||||
- **`experiments`** — скрипты обучения и экспериментов
|
||||
- **`notebooks`** — исследовательские ноутбуки
|
||||
- **`llm`** — основная библиотека с реализацией архитектур LLM (**GPT, GPT-2, LLaMA, Mistral**)
|
||||
- **`hf-proxy`** — экспериментальный адаптер для интеграции с HuggingFace (загрузка, токенизация, экспериментальные скрипты). Функционал может изменяться и не гарантирует полной совместимости с будущими версиями HuggingFace Transformers.
|
||||
- **`experiments`** — скрипты обучения и генерации (включая HF и собственные модели)
|
||||
- **`notebooks`** — исследовательские ноутбуки, анализ архитектур
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
@@ -24,16 +25,29 @@ llm-arch-research/
|
||||
│ └── src/llm/
|
||||
│ ├── core/ # базовые компоненты
|
||||
│ │ ├── base_model.py
|
||||
│ │ ├── cached_decoder.py # Декодер с кэшированием
|
||||
│ │ ├── decoder.py
|
||||
│ │ ├── multi_head_attention.py
|
||||
│ │ ├── head_attention.py
|
||||
│ │ ├── feed_forward.py
|
||||
│ │ ├── token_embeddings.py
|
||||
│ │ └── positional_embeddings.py
|
||||
│ ├── models/gpt/ # GPT и GPT-2 реализация
|
||||
│ │ ├── gpt.py
|
||||
│ │ ├── gpt2.py
|
||||
│ │ └── __init__.py
|
||||
│ │ ├── positional_embeddings.py
|
||||
│ │ ├── rope.py # Rotary Positional Embeddings
|
||||
│ │ ├── rms_norm.py # RMS Normalization
|
||||
│ │ ├── swi_glu.py # SwiGLU активация
|
||||
│ │ ├── silu.py # SiLU активация
|
||||
│ │ └── gelu.py # GELU активация
|
||||
│ ├── models/ # Реализации моделей
|
||||
│ │ ├── gpt/ # GPT и GPT-2 архитектуры
|
||||
│ │ │ ├── gpt.py
|
||||
│ │ │ ├── gpt2.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── llama/ # LLaMA архитектура
|
||||
│ │ │ ├── llama.py
|
||||
│ │ │ └── __init__.py
|
||||
│ │ └── mistral/ # Mistral архитектура
|
||||
│ │ ├── mistral.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── training/ # утилиты обучения
|
||||
│ │ ├── dataset.py
|
||||
│ │ ├── trainer.py
|
||||
@@ -71,6 +85,18 @@ llm-arch-research/
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
**Пример запуска обучения и генерации для любых архитектур:**
|
||||
|
||||
```bash
|
||||
python experiments/llm_only/run_llm_experiment.py --model mistral --action generate --config experiments/llm_only/configs/mistral_generate.json
|
||||
```
|
||||
|
||||
**Использование собственных моделей с HuggingFace-интерфейсом:**
|
||||
```python
|
||||
from hf_proxy.hf_adapter import HFAdapter
|
||||
hf_model = HFAdapter("mistralai/Mistral-7B-v0.1")
|
||||
```
|
||||
|
||||
### Установка зависимостей
|
||||
|
||||
```bash
|
||||
@@ -81,15 +107,17 @@ uv sync
|
||||
uv sync --extra dev
|
||||
```
|
||||
|
||||
### Запуск обучения GPT
|
||||
## ⚡ Работа с экспериментами (experiments/llm_only, experiments/hf_integration)
|
||||
|
||||
```bash
|
||||
# Обучение базовой GPT модели
|
||||
uv run python experiments/llm_only/train_gpt_bpe.py
|
||||
- В `experiments/llm_only`: универсальный скрипт для обучения и генерации LLM (включая LLaMA и Mistral) без HuggingFace — всё через собственную реализацию.
|
||||
- В `experiments/hf_integration`: скрипты и примеры для генерации, обучения и тестирования моделей с помощью HuggingFace API (через hf-proxy). Позволяет использовать свои модели и токенизаторы как стандартные HF-объекты.
|
||||
|
||||
# Обучение с интеграцией HuggingFace
|
||||
uv run python experiments/hf_integration/simple_hf_training.py
|
||||
```
|
||||
**Для моделей Mistral/Llama доступны оба сценария: прямая работа или через HuggingFace-прокси.**
|
||||
|
||||
*Конфиги и примеры см. в соответствующих папках.*
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Тестирование hf-proxy
|
||||
|
||||
@@ -202,33 +230,23 @@ dependencies = [
|
||||
|
||||
## 🎯 Реализованные возможности
|
||||
|
||||
### Архитектуры GPT и GPT-2
|
||||
- ✅ Токенные и позиционные эмбеддинги
|
||||
- ✅ Многоголовое внимание с causal mask
|
||||
- ✅ Декодерные блоки с residual connections
|
||||
- ✅ Layer normalization
|
||||
- ✅ Dropout регуляризация
|
||||
- ✅ Отдельные реализации GPT и GPT-2 (различия в масштабе и деталях архитектуры)
|
||||
### Архитектуры
|
||||
- ✅ GPT, GPT-2: Полностью воспроизводимые реализации, токенные и позиционные эмбеддинги, causal multi-head attention, LayerNorm
|
||||
- ✅ LLaMA: Rotary Positional Embeddings (RoPE), RMSNorm, SwiGLU, оптимизированная память
|
||||
- ✅ Mistral: Sliding Window Attention (оконное внимание), Grouped Query Attention (GQA), совместимость с HF
|
||||
- ✅ Все архитектуры поддерживают обучение и генерацию текста
|
||||
|
||||
### Генерация текста
|
||||
- ✅ Жадный поиск (greedy decoding)
|
||||
- ✅ Вероятностное сэмплирование
|
||||
- ✅ Top-k сэмплирование
|
||||
- ✅ Nucleus sampling (top-p)
|
||||
- ✅ Контроль температуры
|
||||
- ✅ Greedy, sampling (Top-k, Top-p), контроль температуры, efficient caching
|
||||
|
||||
### Обучение
|
||||
- ✅ Датасет для языкового моделирования
|
||||
- ✅ Базовый тренировочный цикл
|
||||
- ✅ Оптимизатор AdamW
|
||||
- ✅ Сохранение чекпоинтов
|
||||
- ✅ Языковое моделирование с кастомными и HF-токенизаторами
|
||||
- ✅ AdamW, кастомные датасеты, сохранение чекпоинтов
|
||||
|
||||
### Интеграция с HuggingFace (hf-proxy)
|
||||
- ✅ Адаптер моделей для совместимости с HF интерфейсами
|
||||
- ✅ Адаптер токенизаторов с поддержкой всех методов HF
|
||||
- ✅ Сохранение и загрузка в HF формате
|
||||
- ✅ Совместимость с HF Trainer и pipelines
|
||||
- ✅ Генерация через стандартные HF интерфейсы
|
||||
- ✅ Экспорт/импорт моделей и токенизаторов в HF совместимый формат
|
||||
- ✅ Генерация и обучение через HF Trainer, pipelines и т.д.
|
||||
- ✅ Двусторонняя поддержка: собственные модели становятся HF-совместимыми и наоборот
|
||||
|
||||
## 🔬 Эксперименты с hf-proxy
|
||||
|
||||
|
||||
@@ -14,54 +14,50 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from hf_proxy import HFAdapter, HFTokenizerAdapter, create_hf_pipeline
|
||||
|
||||
from shared.configs import (
|
||||
TEST_PROMPTS, GENERATION_CONFIG, PATHS
|
||||
)
|
||||
from shared.data import (
|
||||
print_experiment_info, ensure_directories, ExperimentLogger
|
||||
)
|
||||
from shared.configs import TEST_PROMPTS, GENERATION_CONFIG, PATHS
|
||||
from shared.data import print_experiment_info, ensure_directories, ExperimentLogger
|
||||
|
||||
|
||||
def load_hf_model_and_tokenizer() -> tuple:
|
||||
"""
|
||||
Загружает модель и токенизатор в формате HuggingFace.
|
||||
|
||||
|
||||
Returns:
|
||||
tuple: (hf_model, hf_tokenizer, model_config)
|
||||
"""
|
||||
# Используем упрощенную версию модели
|
||||
model_path = "checkpoints/hf_simple_trained"
|
||||
tokenizer_path = "checkpoints/hf_simple_tokenizer"
|
||||
|
||||
|
||||
# Проверяем существование файлов
|
||||
if not os.path.exists(model_path):
|
||||
raise FileNotFoundError(
|
||||
f"Модель не найдена: {model_path}\n"
|
||||
f"Сначала обучите модель: uv run python experiments/hf_integration/simple_hf_training.py"
|
||||
)
|
||||
|
||||
|
||||
if not os.path.exists(tokenizer_path):
|
||||
raise FileNotFoundError(
|
||||
f"Токенизатор не найден: {tokenizer_path}"
|
||||
)
|
||||
|
||||
raise FileNotFoundError(f"Токенизатор не найден: {tokenizer_path}")
|
||||
|
||||
# Загружаем адаптированный токенизатор
|
||||
print("🔧 Загрузка адаптированного токенизатора...")
|
||||
hf_tokenizer = HFTokenizerAdapter.from_pretrained(tokenizer_path)
|
||||
print(f"✅ Токенизатор загружен (vocab_size={hf_tokenizer.vocab_size})")
|
||||
|
||||
|
||||
# Загружаем конфигурацию модели
|
||||
import json
|
||||
|
||||
config_path = os.path.join(model_path, "config.json")
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
model_config = json.load(f)
|
||||
|
||||
|
||||
# Загружаем модель через HFAdapter с правильной конфигурацией
|
||||
print("🔧 Загрузка адаптированной модели...")
|
||||
model_bin_path = os.path.join(model_path, "pytorch_model.bin")
|
||||
|
||||
|
||||
# Создаем конфигурацию из сохраненного config.json
|
||||
from hf_proxy import HFAdapterConfig
|
||||
|
||||
hf_config = HFAdapterConfig(
|
||||
vocab_size=model_config["vocab_size"],
|
||||
hidden_size=model_config["hidden_size"],
|
||||
@@ -69,26 +65,28 @@ def load_hf_model_and_tokenizer() -> tuple:
|
||||
num_attention_heads=model_config["num_attention_heads"],
|
||||
max_position_embeddings=model_config["max_position_embeddings"],
|
||||
hidden_dropout_prob=model_config.get("hidden_dropout_prob", 0.1),
|
||||
attention_probs_dropout_prob=model_config.get("attention_probs_dropout_prob", 0.1),
|
||||
attention_probs_dropout_prob=model_config.get(
|
||||
"attention_probs_dropout_prob", 0.1
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
hf_model = HFAdapter.from_pretrained(model_bin_path, hf_config=hf_config)
|
||||
hf_model.eval()
|
||||
print("✅ Модель загружена")
|
||||
|
||||
|
||||
return hf_model, hf_tokenizer, model_config
|
||||
|
||||
|
||||
def test_hf_pipeline(hf_model, hf_tokenizer):
|
||||
"""
|
||||
Тестирует создание HuggingFace pipeline.
|
||||
|
||||
|
||||
Args:
|
||||
hf_model: Адаптированная модель
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
"""
|
||||
print("\n🧪 Тестирование HuggingFace pipeline...")
|
||||
|
||||
|
||||
try:
|
||||
# Создаем pipeline
|
||||
pipe = create_hf_pipeline(
|
||||
@@ -97,23 +95,23 @@ def test_hf_pipeline(hf_model, hf_tokenizer):
|
||||
device="cpu",
|
||||
max_length=50,
|
||||
do_sample=True,
|
||||
temperature=0.7
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
|
||||
print("✅ HuggingFace pipeline создан")
|
||||
|
||||
|
||||
# Тестируем pipeline
|
||||
test_prompts = TEST_PROMPTS[:3]
|
||||
|
||||
|
||||
for prompt in test_prompts:
|
||||
print(f"\n🔤 Промпт: '{prompt}'")
|
||||
|
||||
|
||||
try:
|
||||
result = pipe(prompt, max_new_tokens=20)
|
||||
print(f"🎯 Результат: {result[0]['generated_text']}")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в pipeline: {e}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка создания pipeline: {e}")
|
||||
|
||||
@@ -121,47 +119,49 @@ def test_hf_pipeline(hf_model, hf_tokenizer):
|
||||
def generate_with_hf_model(hf_model, hf_tokenizer, prompt: str, config: dict) -> str:
|
||||
"""
|
||||
Генерирует текст через адаптированную модель HF.
|
||||
|
||||
|
||||
Args:
|
||||
hf_model: Адаптированная модель
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
prompt: Входной текст
|
||||
config: Конфигурация генерации
|
||||
|
||||
|
||||
Returns:
|
||||
str: Сгенерированный текст
|
||||
"""
|
||||
print(f"🔤 Промпт: '{prompt}'")
|
||||
print(f"📊 Параметры: max_tokens={config['max_new_tokens']}, "
|
||||
f"temp={config['temperature']}, sample={config['do_sample']}")
|
||||
|
||||
print(
|
||||
f"📊 Параметры: max_tokens={config['max_new_tokens']}, "
|
||||
f"temp={config['temperature']}, sample={config['do_sample']}"
|
||||
)
|
||||
|
||||
# Кодируем через адаптированный токенизатор
|
||||
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||
|
||||
|
||||
print(f"🎯 Токены промпта: {inputs['input_ids'].tolist()[0]}")
|
||||
print("🔄 Генерация через HF адаптер...")
|
||||
|
||||
|
||||
# Генерируем через адаптированную модель
|
||||
with torch.no_grad():
|
||||
generated_ids = hf_model.generate(
|
||||
input_ids=inputs['input_ids'],
|
||||
input_ids=inputs["input_ids"],
|
||||
max_new_tokens=config["max_new_tokens"],
|
||||
do_sample=config["do_sample"],
|
||||
temperature=config["temperature"],
|
||||
top_k=config["top_k"],
|
||||
top_p=config["top_p"]
|
||||
top_p=config["top_p"],
|
||||
)
|
||||
|
||||
|
||||
# Декодируем через адаптированный токенизатор
|
||||
generated_text = hf_tokenizer.decode(generated_ids[0], skip_special_tokens=True)
|
||||
|
||||
|
||||
return generated_text
|
||||
|
||||
|
||||
def test_different_hf_strategies(hf_model, hf_tokenizer, prompt: str):
|
||||
"""
|
||||
Тестирует разные стратегии генерации через HF интерфейс.
|
||||
|
||||
|
||||
Args:
|
||||
hf_model: Адаптированная модель
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
@@ -169,32 +169,38 @@ def test_different_hf_strategies(hf_model, hf_tokenizer, prompt: str):
|
||||
"""
|
||||
print(f"\n🎭 Сравнение стратегий генерации через HF для промпта: '{prompt}'")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
strategies = [
|
||||
{"name": "🎯 Жадный поиск", "do_sample": False, "temperature": 1.0},
|
||||
{"name": "🎲 Вероятностная (temp=0.7)", "do_sample": True, "temperature": 0.7},
|
||||
{"name": "🔥 Случайная (temp=1.2)", "do_sample": True, "temperature": 1.2},
|
||||
{"name": "❄️ Детерминированная (temp=0.3)", "do_sample": True, "temperature": 0.3},
|
||||
{
|
||||
"name": "❄️ Детерминированная (temp=0.3)",
|
||||
"do_sample": True,
|
||||
"temperature": 0.3,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
for strategy in strategies:
|
||||
print(f"\n{strategy['name']}:")
|
||||
try:
|
||||
config = GENERATION_CONFIG.copy()
|
||||
config.update({
|
||||
"do_sample": strategy["do_sample"],
|
||||
"temperature": strategy["temperature"],
|
||||
"max_new_tokens": 20
|
||||
})
|
||||
|
||||
config.update(
|
||||
{
|
||||
"do_sample": strategy["do_sample"],
|
||||
"temperature": strategy["temperature"],
|
||||
"max_new_tokens": 20,
|
||||
}
|
||||
)
|
||||
|
||||
generated = generate_with_hf_model(hf_model, hf_tokenizer, prompt, config)
|
||||
|
||||
|
||||
# Выделяем сгенерированную часть
|
||||
generated_part = generated[len(prompt):]
|
||||
generated_part = generated[len(prompt) :]
|
||||
print(f" 📤 Промпт: '{prompt}'")
|
||||
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f" 📄 Полный текст: '{generated}'")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Ошибка: {e}")
|
||||
|
||||
@@ -202,30 +208,30 @@ def test_different_hf_strategies(hf_model, hf_tokenizer, prompt: str):
|
||||
def analyze_hf_tokenization(hf_tokenizer, texts: list):
|
||||
"""
|
||||
Анализирует токенизацию через адаптированный токенизатор.
|
||||
|
||||
|
||||
Args:
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
texts: Список текстов для анализа
|
||||
"""
|
||||
print(f"\n🔍 Анализ токенизации через HF адаптер:")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
for i, text in enumerate(texts):
|
||||
print(f"\nТекст {i+1}: '{text}'")
|
||||
|
||||
|
||||
# Токенизация через адаптер
|
||||
inputs = hf_tokenizer(text, return_tensors="pt")
|
||||
tokens = inputs['input_ids'].tolist()[0]
|
||||
tokens = inputs["input_ids"].tolist()[0]
|
||||
token_strings = hf_tokenizer.tokenize(text)
|
||||
|
||||
|
||||
print(f" Токены (ID): {tokens}")
|
||||
print(f" Токены (текст): {token_strings}")
|
||||
print(f" Количество токенов: {len(tokens)}")
|
||||
|
||||
|
||||
# Декодирование обратно
|
||||
decoded = hf_tokenizer.decode(tokens)
|
||||
print(f" Декодированный: '{decoded}'")
|
||||
|
||||
|
||||
if text == decoded:
|
||||
print(f" ✅ Декодирование корректно")
|
||||
else:
|
||||
@@ -235,51 +241,55 @@ def analyze_hf_tokenization(hf_tokenizer, texts: list):
|
||||
def interactive_hf_generation(hf_model, hf_tokenizer):
|
||||
"""
|
||||
Режим интерактивной генерации через HF интерфейс.
|
||||
|
||||
|
||||
Args:
|
||||
hf_model: Адаптированная модель
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
"""
|
||||
print(f"\n💬 Интерактивная генерация через HF (для выхода введите 'exit')")
|
||||
print("-" * 60)
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n🔤 Введите промпт: ").strip()
|
||||
|
||||
if user_input.lower() in ['exit', 'quit', 'выход']:
|
||||
|
||||
if user_input.lower() in ["exit", "quit", "выход"]:
|
||||
break
|
||||
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
|
||||
# Запрашиваем параметры
|
||||
try:
|
||||
max_tokens = int(input("📏 Макс. токенов [50]: ") or "50")
|
||||
temperature = float(input("🌡️ Температура [0.7]: ") or "0.7")
|
||||
do_sample_input = input("🎲 Сэмплирование (y/n) [y]: ").lower()
|
||||
do_sample = do_sample_input != 'n'
|
||||
do_sample = do_sample_input != "n"
|
||||
except:
|
||||
max_tokens = 50
|
||||
temperature = 0.7
|
||||
do_sample = True
|
||||
print("⚠️ Использую параметры по умолчанию")
|
||||
|
||||
|
||||
config = GENERATION_CONFIG.copy()
|
||||
config.update({
|
||||
"max_new_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"do_sample": do_sample
|
||||
})
|
||||
|
||||
generated = generate_with_hf_model(hf_model, hf_tokenizer, user_input, config)
|
||||
|
||||
generated_part = generated[len(user_input):]
|
||||
config.update(
|
||||
{
|
||||
"max_new_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"do_sample": do_sample,
|
||||
}
|
||||
)
|
||||
|
||||
generated = generate_with_hf_model(
|
||||
hf_model, hf_tokenizer, user_input, config
|
||||
)
|
||||
|
||||
generated_part = generated[len(user_input) :]
|
||||
print(f"\n🎯 Результат:")
|
||||
print(f" 📤 Промпт: '{user_input}'")
|
||||
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f" 📄 Полный текст: '{generated}'")
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Завершение работы...")
|
||||
break
|
||||
@@ -295,76 +305,79 @@ def main():
|
||||
"model": "GPT через HFAdapter",
|
||||
"tokenizer": "BPE через HFTokenizerAdapter",
|
||||
"инструменты": "HuggingFace pipeline & генерация",
|
||||
"стратегия": "интеграция с HF экосистемой"
|
||||
"стратегия": "интеграция с HF экосистемой",
|
||||
}
|
||||
|
||||
|
||||
print_experiment_info(experiment_name, experiment_config)
|
||||
ensure_directories()
|
||||
logger = ExperimentLogger(experiment_name)
|
||||
|
||||
|
||||
try:
|
||||
# Загружаем модель и токенизатор в HF формате
|
||||
hf_model, hf_tokenizer, model_config = load_hf_model_and_tokenizer()
|
||||
|
||||
|
||||
# === Анализ токенизации ===
|
||||
analysis_texts = [
|
||||
"Искусственный интеллект",
|
||||
"Нейронные сети",
|
||||
"Машинное обучение"
|
||||
"Нейронные сети",
|
||||
"Машинное обучение",
|
||||
]
|
||||
analyze_hf_tokenization(hf_tokenizer, analysis_texts)
|
||||
|
||||
|
||||
# === Тестирование HF pipeline ===
|
||||
test_hf_pipeline(hf_model, hf_tokenizer)
|
||||
|
||||
|
||||
# === Генерация с разными промптами ===
|
||||
print(f"\n🎯 Генерация текста через HF адаптер")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
for i, prompt in enumerate(TEST_PROMPTS):
|
||||
print(f"\n📝 Пример {i+1}/{len(TEST_PROMPTS)}")
|
||||
print("-" * 40)
|
||||
|
||||
|
||||
try:
|
||||
generated = generate_with_hf_model(hf_model, hf_tokenizer, prompt, GENERATION_CONFIG)
|
||||
|
||||
generated = generate_with_hf_model(
|
||||
hf_model, hf_tokenizer, prompt, GENERATION_CONFIG
|
||||
)
|
||||
|
||||
# Выделяем сгенерированную часть
|
||||
generated_part = generated[len(prompt):]
|
||||
|
||||
generated_part = generated[len(prompt) :]
|
||||
|
||||
print(f"📤 Промпт: '{prompt}'")
|
||||
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f"📄 Полный текст: '{generated}'")
|
||||
print(f"📏 Длина: {len(generated)} символов")
|
||||
|
||||
|
||||
# Логируем успешную генерацию
|
||||
logger.log_metric(f"hf_generation_length_{i}", len(generated))
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при генерации: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# === Сравнение стратегий генерации ===
|
||||
test_prompt = "Искусственный"
|
||||
test_different_hf_strategies(hf_model, hf_tokenizer, test_prompt)
|
||||
|
||||
|
||||
# === Интерактивная генерация ===
|
||||
interactive_hf_generation(hf_model, hf_tokenizer)
|
||||
|
||||
|
||||
# === Сохранение результатов ===
|
||||
logger.save_logs("checkpoints/hf_integration_generation_logs.json")
|
||||
|
||||
|
||||
print(f"\n🎉 Эксперимент с HF интеграцией завершен успешно!")
|
||||
print(f"\n📚 Достигнутая интеграция:")
|
||||
print(f" ✅ Загрузка модели и токенизатора в HF формате")
|
||||
print(f" ✅ Использование HF pipeline")
|
||||
print(f" ✅ Генерация через стандартные HF интерфейсы")
|
||||
print(f" ✅ Совместимость с HF экосистемой")
|
||||
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"❌ {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в эксперименте: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
||||
@@ -19,141 +19,139 @@ from llm.tokenizers import BPETokenizer
|
||||
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||
|
||||
from shared.configs import (
|
||||
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||
TRAINING_CONFIG, PATHS, TEST_PROMPTS
|
||||
TRAIN_TEXTS,
|
||||
BASE_GPT_CONFIG,
|
||||
BPE_CONFIG,
|
||||
TRAINING_CONFIG,
|
||||
PATHS,
|
||||
TEST_PROMPTS,
|
||||
)
|
||||
|
||||
|
||||
def create_dataset(hf_tokenizer, texts, max_length=128):
|
||||
"""
|
||||
Создает простой датасет для обучения.
|
||||
|
||||
|
||||
Args:
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
texts: Список текстов
|
||||
max_length: Максимальная длина последовательности
|
||||
|
||||
|
||||
Returns:
|
||||
list: Список тензоров input_ids
|
||||
"""
|
||||
dataset = []
|
||||
|
||||
|
||||
for text in texts:
|
||||
# Токенизируем текст
|
||||
inputs = hf_tokenizer(
|
||||
text,
|
||||
max_length=max_length,
|
||||
text,
|
||||
max_length=max_length,
|
||||
truncation=True,
|
||||
padding=False,
|
||||
return_tensors="pt"
|
||||
return_tensors="pt",
|
||||
)
|
||||
|
||||
input_ids = inputs['input_ids'][0]
|
||||
|
||||
|
||||
input_ids = inputs["input_ids"][0]
|
||||
|
||||
# Создаем метки для языкового моделирования
|
||||
labels = input_ids.clone()
|
||||
|
||||
dataset.append({
|
||||
'input_ids': input_ids,
|
||||
'labels': labels
|
||||
})
|
||||
|
||||
|
||||
dataset.append({"input_ids": input_ids, "labels": labels})
|
||||
|
||||
return dataset
|
||||
|
||||
|
||||
def manual_training_loop(hf_model, hf_tokenizer, train_texts, val_texts, config):
|
||||
"""
|
||||
Ручной цикл обучения без использования Trainer.
|
||||
|
||||
|
||||
Args:
|
||||
hf_model: Адаптированная модель
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
train_texts: Тексты для обучения
|
||||
val_texts: Тексты для валидации
|
||||
config: Конфигурация обучения
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Результаты обучения
|
||||
"""
|
||||
print("🎯 Запуск ручного обучения...")
|
||||
|
||||
|
||||
# Создаем датасеты
|
||||
train_dataset = create_dataset(hf_tokenizer, train_texts)
|
||||
val_dataset = create_dataset(hf_tokenizer, val_texts)
|
||||
|
||||
|
||||
print(f"📊 Данные: {len(train_dataset)} train, {len(val_dataset)} validation")
|
||||
|
||||
|
||||
# Оптимизатор
|
||||
optimizer = torch.optim.AdamW(
|
||||
hf_model.parameters(),
|
||||
lr=config["learning_rate"]
|
||||
)
|
||||
|
||||
optimizer = torch.optim.AdamW(hf_model.parameters(), lr=config["learning_rate"])
|
||||
|
||||
# Функция потерь
|
||||
loss_fn = nn.CrossEntropyLoss()
|
||||
|
||||
|
||||
# Обучение
|
||||
hf_model.train()
|
||||
train_losses = []
|
||||
val_losses = []
|
||||
|
||||
|
||||
for epoch in range(config["num_epochs"]):
|
||||
print(f"\n📅 Эпоха {epoch + 1}/{config['num_epochs']}")
|
||||
|
||||
|
||||
# Обучение
|
||||
epoch_train_loss = 0
|
||||
for i, batch in enumerate(train_dataset):
|
||||
optimizer.zero_grad()
|
||||
|
||||
input_ids = batch['input_ids'].unsqueeze(0) # [1, seq_len]
|
||||
labels = batch['labels'].unsqueeze(0) # [1, seq_len]
|
||||
|
||||
|
||||
input_ids = batch["input_ids"].unsqueeze(0) # [1, seq_len]
|
||||
labels = batch["labels"].unsqueeze(0) # [1, seq_len]
|
||||
|
||||
# Forward pass
|
||||
outputs = hf_model(input_ids=input_ids, labels=labels)
|
||||
loss = outputs.loss
|
||||
|
||||
|
||||
# Backward pass
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
|
||||
epoch_train_loss += loss.item()
|
||||
|
||||
|
||||
if i % 5 == 0:
|
||||
print(f" Batch {i}/{len(train_dataset)}: loss = {loss.item():.4f}")
|
||||
|
||||
|
||||
avg_train_loss = epoch_train_loss / len(train_dataset)
|
||||
train_losses.append(avg_train_loss)
|
||||
print(f" 📊 Средняя train loss: {avg_train_loss:.4f}")
|
||||
|
||||
|
||||
# Валидация
|
||||
hf_model.eval()
|
||||
epoch_val_loss = 0
|
||||
with torch.no_grad():
|
||||
for batch in val_dataset:
|
||||
input_ids = batch['input_ids'].unsqueeze(0)
|
||||
labels = batch['labels'].unsqueeze(0)
|
||||
|
||||
input_ids = batch["input_ids"].unsqueeze(0)
|
||||
labels = batch["labels"].unsqueeze(0)
|
||||
|
||||
outputs = hf_model(input_ids=input_ids, labels=labels)
|
||||
epoch_val_loss += outputs.loss.item()
|
||||
|
||||
|
||||
avg_val_loss = epoch_val_loss / len(val_dataset)
|
||||
val_losses.append(avg_val_loss)
|
||||
print(f" 📊 Средняя val loss: {avg_val_loss:.4f}")
|
||||
|
||||
|
||||
hf_model.train()
|
||||
|
||||
|
||||
return {
|
||||
'train_losses': train_losses,
|
||||
'val_losses': val_losses,
|
||||
'final_train_loss': train_losses[-1],
|
||||
'final_val_loss': val_losses[-1]
|
||||
"train_losses": train_losses,
|
||||
"val_losses": val_losses,
|
||||
"final_train_loss": train_losses[-1],
|
||||
"final_val_loss": val_losses[-1],
|
||||
}
|
||||
|
||||
|
||||
def test_generation_after_training(hf_model, hf_tokenizer, test_prompts):
|
||||
"""
|
||||
Тестирует генерацию после обучения.
|
||||
|
||||
|
||||
Args:
|
||||
hf_model: Обученная модель
|
||||
hf_tokenizer: Токенизатор
|
||||
@@ -161,24 +159,24 @@ def test_generation_after_training(hf_model, hf_tokenizer, test_prompts):
|
||||
"""
|
||||
print("\n🧪 Тестирование генерации после обучения...")
|
||||
hf_model.eval()
|
||||
|
||||
|
||||
for prompt in test_prompts[:3]:
|
||||
print(f"\n🔤 Промпт: '{prompt}'")
|
||||
|
||||
|
||||
try:
|
||||
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
generated = hf_model.generate(
|
||||
input_ids=inputs['input_ids'],
|
||||
input_ids=inputs["input_ids"],
|
||||
max_new_tokens=20,
|
||||
do_sample=True,
|
||||
temperature=0.8
|
||||
temperature=0.8,
|
||||
)
|
||||
|
||||
|
||||
generated_text = hf_tokenizer.decode(generated[0], skip_special_tokens=True)
|
||||
print(f"🎯 Результат: '{generated_text}'")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка генерации: {e}")
|
||||
|
||||
@@ -188,96 +186,102 @@ def main():
|
||||
print("=" * 60)
|
||||
print("🚀 УПРОЩЕННОЕ ОБУЧЕНИЕ GPT С HF-PROXY")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
try:
|
||||
# === Подготовка данных ===
|
||||
print("🔧 Подготовка данных...")
|
||||
train_texts = TRAIN_TEXTS[:10] # Используем меньше данных для быстрого тестирования
|
||||
train_texts = TRAIN_TEXTS[
|
||||
:10
|
||||
] # Используем меньше данных для быстрого тестирования
|
||||
val_texts = TRAIN_TEXTS[10:12]
|
||||
|
||||
|
||||
print(f"📊 Данные: {len(train_texts)} train, {len(val_texts)} validation")
|
||||
|
||||
|
||||
# === Подготовка токенизатора ===
|
||||
print("🔧 Подготовка токенизатора...")
|
||||
llm_tokenizer = BPETokenizer()
|
||||
llm_tokenizer.train(
|
||||
texts=train_texts,
|
||||
vocab_size=BPE_CONFIG["vocab_size"],
|
||||
special_tokens=BPE_CONFIG["special_tokens"]
|
||||
special_tokens=BPE_CONFIG["special_tokens"],
|
||||
)
|
||||
|
||||
|
||||
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||
print(f"✅ Токенизатор создан (vocab_size={hf_tokenizer.vocab_size})")
|
||||
|
||||
|
||||
# === Подготовка модели ===
|
||||
print("🔧 Подготовка модели...")
|
||||
model_config = BASE_GPT_CONFIG.copy()
|
||||
model_config["vocab_size"] = hf_tokenizer.vocab_size
|
||||
|
||||
|
||||
llm_model = GPT(model_config)
|
||||
hf_model = HFAdapter.from_llm_model(llm_model)
|
||||
print(f"✅ Модель создана")
|
||||
|
||||
|
||||
# === Тестирование до обучения ===
|
||||
print("\n🧪 Тестирование до обучения...")
|
||||
test_generation_after_training(hf_model, hf_tokenizer, TEST_PROMPTS)
|
||||
|
||||
|
||||
# === Обучение ===
|
||||
print(f"\n🎯 Обучение модели...")
|
||||
training_config = {
|
||||
"learning_rate": TRAINING_CONFIG["learning_rate"],
|
||||
"num_epochs": 2, # Меньше эпох для быстрого тестирования
|
||||
"batch_size": TRAINING_CONFIG["batch_size"]
|
||||
"batch_size": TRAINING_CONFIG["batch_size"],
|
||||
}
|
||||
|
||||
|
||||
results = manual_training_loop(
|
||||
hf_model, hf_tokenizer, train_texts, val_texts, training_config
|
||||
)
|
||||
|
||||
|
||||
print(f"\n📊 Результаты обучения:")
|
||||
print(f" Final train loss: {results['final_train_loss']:.4f}")
|
||||
print(f" Final val loss: {results['final_val_loss']:.4f}")
|
||||
|
||||
|
||||
# === Тестирование после обучения ===
|
||||
print("\n🧪 Тестирование после обучения...")
|
||||
test_generation_after_training(hf_model, hf_tokenizer, TEST_PROMPTS)
|
||||
|
||||
|
||||
# === Сохранение модели ===
|
||||
print(f"\n💾 Сохранение модели...")
|
||||
|
||||
|
||||
# Создаем директории
|
||||
os.makedirs("checkpoints/hf_simple_trained", exist_ok=True)
|
||||
os.makedirs("checkpoints/hf_simple_tokenizer", exist_ok=True)
|
||||
|
||||
|
||||
# Сохраняем токенизатор
|
||||
hf_tokenizer.save_pretrained("checkpoints/hf_simple_tokenizer")
|
||||
print("✅ Токенизатор сохранен")
|
||||
|
||||
|
||||
# Сохраняем модель
|
||||
HFAdapter.save_pretrained(
|
||||
hf_model,
|
||||
"checkpoints/hf_simple_trained",
|
||||
tokenizer=hf_tokenizer
|
||||
hf_model, "checkpoints/hf_simple_trained", tokenizer=hf_tokenizer
|
||||
)
|
||||
print("✅ Модель сохранена")
|
||||
|
||||
|
||||
# Сохраняем результаты
|
||||
results_path = "checkpoints/simple_training_results.json"
|
||||
with open(results_path, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
'training_config': training_config,
|
||||
'model_config': model_config,
|
||||
'results': results
|
||||
}, f, indent=2, ensure_ascii=False)
|
||||
with open(results_path, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"training_config": training_config,
|
||||
"model_config": model_config,
|
||||
"results": results,
|
||||
},
|
||||
f,
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
print(f"✅ Результаты сохранены в {results_path}")
|
||||
|
||||
|
||||
print(f"\n🎉 Упрощенное обучение завершено успешно!")
|
||||
print(f"\n💡 Для использования обученной модели:")
|
||||
print(f" uv run python experiments/hf_integration/generate_with_hf_tools.py")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в эксперименте: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
||||
@@ -16,158 +16,163 @@ from llm.tokenizers import BPETokenizer
|
||||
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||
|
||||
from shared.configs import (
|
||||
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||
TEST_PROMPTS, GENERATION_CONFIG
|
||||
TRAIN_TEXTS,
|
||||
BASE_GPT_CONFIG,
|
||||
BPE_CONFIG,
|
||||
TEST_PROMPTS,
|
||||
GENERATION_CONFIG,
|
||||
)
|
||||
|
||||
|
||||
def test_basic_hf_integration():
|
||||
"""Тестирует базовую интеграцию hf-proxy."""
|
||||
print("🧪 Тестирование базовой интеграции hf-proxy...")
|
||||
|
||||
|
||||
# === Подготовка токенизатора ===
|
||||
print("1. Подготовка токенизатора...")
|
||||
llm_tokenizer = BPETokenizer()
|
||||
llm_tokenizer.train(
|
||||
texts=TRAIN_TEXTS,
|
||||
vocab_size=BPE_CONFIG["vocab_size"],
|
||||
special_tokens=BPE_CONFIG["special_tokens"]
|
||||
special_tokens=BPE_CONFIG["special_tokens"],
|
||||
)
|
||||
|
||||
|
||||
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||
print(f" ✅ Токенизатор создан (vocab_size={hf_tokenizer.vocab_size})")
|
||||
|
||||
|
||||
# === Подготовка модели ===
|
||||
print("2. Подготовка модели...")
|
||||
model_config = BASE_GPT_CONFIG.copy()
|
||||
model_config["vocab_size"] = hf_tokenizer.vocab_size
|
||||
|
||||
|
||||
llm_model = GPT(model_config)
|
||||
hf_model = HFAdapter.from_llm_model(llm_model)
|
||||
print(f" ✅ Модель создана")
|
||||
|
||||
|
||||
# === Тестирование токенизации ===
|
||||
print("3. Тестирование токенизации...")
|
||||
test_texts = ["Искусственный интеллект", "Нейронные сети"]
|
||||
|
||||
|
||||
for text in test_texts:
|
||||
print(f" 📝 Текст: '{text}'")
|
||||
|
||||
|
||||
# Оригинальный токенизатор
|
||||
original_tokens = llm_tokenizer.encode(text)
|
||||
print(f" Оригинальный: {len(original_tokens)} токенов")
|
||||
|
||||
|
||||
# HF адаптер
|
||||
hf_inputs = hf_tokenizer(text, return_tensors="pt")
|
||||
print(f" HF адаптер: {hf_inputs['input_ids'].shape}")
|
||||
|
||||
|
||||
# Декодирование
|
||||
decoded = hf_tokenizer.decode(hf_inputs['input_ids'][0])
|
||||
decoded = hf_tokenizer.decode(hf_inputs["input_ids"][0])
|
||||
print(f" Декодированный: '{decoded}'")
|
||||
|
||||
|
||||
# === Тестирование forward pass ===
|
||||
print("4. Тестирование forward pass...")
|
||||
for text in test_texts:
|
||||
hf_inputs = hf_tokenizer(text, return_tensors="pt")
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = hf_model(**hf_inputs)
|
||||
|
||||
|
||||
print(f" 📝 '{text}' -> logits: {outputs.logits.shape}")
|
||||
|
||||
|
||||
# === Тестирование генерации ===
|
||||
print("5. Тестирование генерации...")
|
||||
hf_model.eval()
|
||||
|
||||
|
||||
for prompt in TEST_PROMPTS[:3]:
|
||||
print(f" 🔤 Промпт: '{prompt}'")
|
||||
|
||||
|
||||
try:
|
||||
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
generated = hf_model.generate(
|
||||
input_ids=inputs['input_ids'],
|
||||
input_ids=inputs["input_ids"],
|
||||
max_new_tokens=10,
|
||||
do_sample=True,
|
||||
temperature=0.8
|
||||
temperature=0.8,
|
||||
)
|
||||
|
||||
|
||||
generated_text = hf_tokenizer.decode(generated[0], skip_special_tokens=True)
|
||||
print(f" 🎯 Результат: '{generated_text}'")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Ошибка: {e}")
|
||||
|
||||
|
||||
# === Тестирование сохранения/загрузки ===
|
||||
print("6. Тестирование сохранения/загрузки...")
|
||||
try:
|
||||
# Сохраняем токенизатор
|
||||
hf_tokenizer.save_pretrained("test_save/tokenizer")
|
||||
print(" ✅ Токенизатор сохранен")
|
||||
|
||||
|
||||
# Сохраняем модель
|
||||
HFAdapter.save_pretrained(hf_model, "test_save/model", tokenizer=hf_tokenizer)
|
||||
print(" ✅ Модель сохранена")
|
||||
|
||||
|
||||
# Загружаем токенизатор
|
||||
loaded_tokenizer = HFTokenizerAdapter.from_pretrained("test_save/tokenizer")
|
||||
print(f" ✅ Токенизатор загружен (vocab_size={loaded_tokenizer.vocab_size})")
|
||||
|
||||
|
||||
# Загружаем модель
|
||||
model_path = os.path.join("test_save/model", "pytorch_model.bin")
|
||||
loaded_model = HFAdapter.from_pretrained(model_path)
|
||||
print(" ✅ Модель загружена")
|
||||
|
||||
|
||||
# Проверяем работоспособность загруженной модели
|
||||
test_input = hf_tokenizer("Тест", return_tensors="pt")
|
||||
with torch.no_grad():
|
||||
loaded_outputs = loaded_model(**test_input)
|
||||
print(f" ✅ Загруженная модель работает (logits: {loaded_outputs.logits.shape})")
|
||||
|
||||
print(
|
||||
f" ✅ Загруженная модель работает (logits: {loaded_outputs.logits.shape})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Ошибка сохранения/загрузки: {e}")
|
||||
|
||||
|
||||
print("\n🎉 Базовое тестирование hf-proxy завершено!")
|
||||
|
||||
|
||||
def test_hf_tokenizer_methods():
|
||||
"""Тестирует различные методы HF токенизатора."""
|
||||
print("\n🧪 Тестирование методов HF токенизатора...")
|
||||
|
||||
|
||||
# Создаем токенизатор
|
||||
llm_tokenizer = BPETokenizer()
|
||||
llm_tokenizer.train(
|
||||
texts=TRAIN_TEXTS[:5],
|
||||
vocab_size=500,
|
||||
special_tokens=BPE_CONFIG["special_tokens"]
|
||||
special_tokens=BPE_CONFIG["special_tokens"],
|
||||
)
|
||||
|
||||
|
||||
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||
|
||||
|
||||
test_text = "Искусственный интеллект и машинное обучение"
|
||||
|
||||
|
||||
# Тестируем разные методы
|
||||
print("1. Метод __call__:")
|
||||
result = hf_tokenizer(test_text, return_tensors="pt")
|
||||
print(f" Результат: {result}")
|
||||
|
||||
|
||||
print("2. Метод encode:")
|
||||
encoded = hf_tokenizer.encode(test_text)
|
||||
print(f" Закодировано: {encoded}")
|
||||
|
||||
|
||||
print("3. Метод decode:")
|
||||
decoded = hf_tokenizer.decode(encoded)
|
||||
print(f" Декодировано: '{decoded}'")
|
||||
|
||||
|
||||
print("4. Метод tokenize:")
|
||||
tokens = hf_tokenizer.tokenize(test_text)
|
||||
print(f" Токены: {tokens}")
|
||||
|
||||
|
||||
print("5. Метод get_vocab:")
|
||||
vocab = hf_tokenizer.get_vocab()
|
||||
print(f" Размер словаря: {len(vocab)}")
|
||||
|
||||
|
||||
print("✅ Все методы токенизатора работают!")
|
||||
|
||||
|
||||
@@ -176,14 +181,14 @@ def main():
|
||||
print("=" * 60)
|
||||
print("🧪 ТЕСТИРОВАНИЕ HF-PROXY")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
try:
|
||||
# Тестируем базовую интеграцию
|
||||
test_basic_hf_integration()
|
||||
|
||||
|
||||
# Тестируем методы токенизатора
|
||||
test_hf_tokenizer_methods()
|
||||
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 ВСЕ ТЕСТЫ ПРОЙДЕНЫ УСПЕШНО!")
|
||||
print("=" * 60)
|
||||
@@ -195,10 +200,11 @@ def main():
|
||||
print(" ✅ Генерация текста")
|
||||
print(" ✅ Сохранение и загрузка моделей")
|
||||
print(" ✅ Все методы HF токенизатора")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Ошибка в тестировании: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
||||
@@ -17,28 +17,34 @@ from llm.tokenizers import BPETokenizer
|
||||
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||
|
||||
from shared.configs import (
|
||||
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||
TRAINING_CONFIG, PATHS, TEST_PROMPTS
|
||||
TRAIN_TEXTS,
|
||||
BASE_GPT_CONFIG,
|
||||
BPE_CONFIG,
|
||||
TRAINING_CONFIG,
|
||||
PATHS,
|
||||
TEST_PROMPTS,
|
||||
)
|
||||
from shared.data import (
|
||||
load_training_data, ensure_directories,
|
||||
print_experiment_info, ExperimentLogger
|
||||
load_training_data,
|
||||
ensure_directories,
|
||||
print_experiment_info,
|
||||
ExperimentLogger,
|
||||
)
|
||||
|
||||
|
||||
def setup_hf_training():
|
||||
"""
|
||||
Настраивает окружение для обучения через HuggingFace Trainer.
|
||||
|
||||
|
||||
Returns:
|
||||
tuple: (hf_model, hf_tokenizer, llm_tokenizer, model_config)
|
||||
"""
|
||||
print("🔧 Настройка HuggingFace обучения...")
|
||||
|
||||
|
||||
# === Подготовка данных ===
|
||||
train_texts, val_texts = load_training_data()
|
||||
print(f"📊 Данные: {len(train_texts)} train, {len(val_texts)} validation")
|
||||
|
||||
|
||||
# === Обучение/загрузка токенизатора ===
|
||||
if os.path.exists(PATHS["bpe_tokenizer"]):
|
||||
print("📝 Загрузка BPE токенизатора...")
|
||||
@@ -50,55 +56,55 @@ def setup_hf_training():
|
||||
llm_tokenizer.train(
|
||||
texts=TRAIN_TEXTS,
|
||||
vocab_size=BPE_CONFIG["vocab_size"],
|
||||
special_tokens=BPE_CONFIG["special_tokens"]
|
||||
special_tokens=BPE_CONFIG["special_tokens"],
|
||||
)
|
||||
llm_tokenizer.save(PATHS["bpe_tokenizer"])
|
||||
print(f"✅ Токенизатор обучен и сохранен")
|
||||
|
||||
|
||||
# === Создание адаптера токенизатора ===
|
||||
print("🔧 Создание адаптера HuggingFace для токенизатора...")
|
||||
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||
print(f"✅ Адаптер токенизатора создан")
|
||||
|
||||
|
||||
# === Инициализация модели ===
|
||||
model_config = BASE_GPT_CONFIG.copy()
|
||||
model_config["vocab_size"] = llm_tokenizer.get_vocab_size()
|
||||
|
||||
|
||||
print("🔧 Создание GPT модели...")
|
||||
llm_model = GPT(model_config)
|
||||
|
||||
|
||||
# === Создание адаптера модели ===
|
||||
print("🔧 Создание адаптера HuggingFace для модели...")
|
||||
hf_model = HFAdapter.from_llm_model(llm_model)
|
||||
print(f"✅ Адаптер модели создан")
|
||||
|
||||
|
||||
return hf_model, hf_tokenizer, llm_tokenizer, model_config, train_texts, val_texts
|
||||
|
||||
|
||||
def test_hf_integration(hf_model, hf_tokenizer, llm_tokenizer):
|
||||
"""
|
||||
Тестирует интеграцию с HuggingFace инструментами.
|
||||
|
||||
|
||||
Args:
|
||||
hf_model: Адаптированная модель
|
||||
hf_tokenizer: Адаптированный токенизатор
|
||||
llm_tokenizer: Оригинальный токенизатор
|
||||
"""
|
||||
print("\n🧪 Тестирование интеграции с HuggingFace...")
|
||||
|
||||
|
||||
test_texts = ["Искусственный интеллект", "Нейронные сети"]
|
||||
|
||||
|
||||
for text in test_texts:
|
||||
print(f"\n🔤 Текст: '{text}'")
|
||||
|
||||
|
||||
# Тестируем адаптированный токенизатор
|
||||
hf_inputs = hf_tokenizer(text, return_tensors="pt")
|
||||
print(f" HF токенизатор: {hf_inputs['input_ids'].shape}")
|
||||
|
||||
|
||||
# Тестируем оригинальный токенизатор для сравнения
|
||||
original_tokens = llm_tokenizer.encode(text)
|
||||
print(f" Оригинальный токенизатор: {len(original_tokens)} токенов")
|
||||
|
||||
|
||||
# Тестируем forward pass через адаптированную модель
|
||||
try:
|
||||
with torch.no_grad():
|
||||
@@ -114,28 +120,35 @@ def main():
|
||||
experiment_name = "Обучение GPT через HF Trainer (с hf-proxy)"
|
||||
experiment_config = {
|
||||
"model": "GPT через HFAdapter",
|
||||
"tokenizer": "BPE через HFTokenizerAdapter",
|
||||
"tokenizer": "BPE через HFTokenizerAdapter",
|
||||
"trainer": "HuggingFace Trainer",
|
||||
"vocab_size": BPE_CONFIG["vocab_size"],
|
||||
"training_epochs": TRAINING_CONFIG["num_epochs"]
|
||||
"training_epochs": TRAINING_CONFIG["num_epochs"],
|
||||
}
|
||||
|
||||
|
||||
print_experiment_info(experiment_name, experiment_config)
|
||||
ensure_directories()
|
||||
logger = ExperimentLogger(experiment_name)
|
||||
|
||||
|
||||
try:
|
||||
# Настраиваем обучение
|
||||
hf_model, hf_tokenizer, llm_tokenizer, model_config, train_texts, val_texts = setup_hf_training()
|
||||
|
||||
(
|
||||
hf_model,
|
||||
hf_tokenizer,
|
||||
llm_tokenizer,
|
||||
model_config,
|
||||
train_texts,
|
||||
val_texts,
|
||||
) = setup_hf_training()
|
||||
|
||||
# Тестируем интеграцию
|
||||
test_hf_integration(hf_model, hf_tokenizer, llm_tokenizer)
|
||||
|
||||
|
||||
# === Подготовка датасетов HuggingFace ===
|
||||
print(f"\n📊 Подготовка датасетов HuggingFace...")
|
||||
|
||||
|
||||
from datasets import Dataset
|
||||
|
||||
|
||||
def tokenize_function(examples):
|
||||
"""Функция токенизации для HF datasets."""
|
||||
# Используем адаптированный токенизатор
|
||||
@@ -147,11 +160,11 @@ def main():
|
||||
)
|
||||
tokenized["labels"] = tokenized["input_ids"].copy()
|
||||
return tokenized
|
||||
|
||||
|
||||
# Создаем датасеты
|
||||
train_dataset = Dataset.from_dict({"text": train_texts})
|
||||
val_dataset = Dataset.from_dict({"text": val_texts})
|
||||
|
||||
|
||||
# Токенизируем
|
||||
train_dataset = train_dataset.map(
|
||||
tokenize_function,
|
||||
@@ -163,26 +176,26 @@ def main():
|
||||
batched=True,
|
||||
remove_columns=val_dataset.column_names,
|
||||
)
|
||||
|
||||
|
||||
print(f" Train датасет: {len(train_dataset)} примеров")
|
||||
print(f" Validation датасет: {len(val_dataset)} примеров")
|
||||
|
||||
|
||||
# === Настройка HuggingFace Trainer ===
|
||||
print(f"\n🔧 Настройка HuggingFace Trainer...")
|
||||
|
||||
|
||||
from transformers import (
|
||||
Trainer,
|
||||
Trainer,
|
||||
TrainingArguments,
|
||||
DataCollatorForLanguageModeling
|
||||
DataCollatorForLanguageModeling,
|
||||
)
|
||||
|
||||
|
||||
# Data collator для языкового моделирования
|
||||
data_collator = DataCollatorForLanguageModeling(
|
||||
tokenizer=hf_tokenizer,
|
||||
mlm=False,
|
||||
pad_to_multiple_of=8,
|
||||
)
|
||||
|
||||
|
||||
# Аргументы обучения
|
||||
training_args = TrainingArguments(
|
||||
output_dir=PATHS["hf_model"],
|
||||
@@ -204,7 +217,7 @@ def main():
|
||||
dataloader_pin_memory=False,
|
||||
report_to=None,
|
||||
)
|
||||
|
||||
|
||||
# Создаем Trainer
|
||||
trainer = Trainer(
|
||||
model=hf_model,
|
||||
@@ -213,84 +226,87 @@ def main():
|
||||
eval_dataset=val_dataset,
|
||||
data_collator=data_collator,
|
||||
)
|
||||
|
||||
|
||||
print("✅ HuggingFace Trainer настроен")
|
||||
|
||||
|
||||
# === Запуск обучения ===
|
||||
print(f"\n🎯 Запуск обучения через HuggingFace Trainer...")
|
||||
|
||||
|
||||
train_result = trainer.train()
|
||||
|
||||
|
||||
# Сохраняем лучшую модель
|
||||
trainer.save_model()
|
||||
hf_tokenizer.save_pretrained(PATHS["hf_model"])
|
||||
|
||||
|
||||
print("✅ Обучение завершено успешно!")
|
||||
print(f"📊 Final train loss: {train_result.metrics['train_loss']:.4f}")
|
||||
|
||||
|
||||
if "eval_loss" in train_result.metrics:
|
||||
print(f"📊 Final eval loss: {train_result.metrics['eval_loss']:.4f}")
|
||||
|
||||
|
||||
# === Сохранение через hf-proxy ===
|
||||
print(f"\n💾 Сохранение через hf-proxy...")
|
||||
|
||||
|
||||
from hf_proxy import convert_to_hf_format
|
||||
|
||||
|
||||
# Сохраняем токенизатор в HF формате
|
||||
hf_tokenizer_dir = PATHS["hf_tokenizer"]
|
||||
hf_tokenizer.save_pretrained(hf_tokenizer_dir)
|
||||
|
||||
|
||||
# Сохраняем модель через hf-proxy
|
||||
hf_proxy_dir = PATHS["hf_proxy_model"]
|
||||
HFAdapter.save_pretrained(hf_model, hf_proxy_dir, tokenizer=hf_tokenizer)
|
||||
|
||||
|
||||
print(f"✅ Модель сохранена в HF формате:")
|
||||
print(f" - {PATHS['hf_model']}: стандартный HF формат")
|
||||
print(f" - {hf_proxy_dir}: через hf-proxy")
|
||||
print(f" - {hf_tokenizer_dir}: токенизатор в HF формате")
|
||||
|
||||
|
||||
# === Тестирование генерации ===
|
||||
print(f"\n🧪 Тестирование генерации после обучения...")
|
||||
hf_model.eval()
|
||||
|
||||
|
||||
for prompt in TEST_PROMPTS[:3]:
|
||||
print(f"\n🔤 Промпт: '{prompt}'")
|
||||
|
||||
|
||||
try:
|
||||
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
generated = hf_model.generate(
|
||||
input_ids=inputs['input_ids'],
|
||||
input_ids=inputs["input_ids"],
|
||||
max_new_tokens=20,
|
||||
do_sample=True,
|
||||
temperature=0.8
|
||||
temperature=0.8,
|
||||
)
|
||||
|
||||
generated_text = hf_tokenizer.decode(generated[0], skip_special_tokens=True)
|
||||
|
||||
generated_text = hf_tokenizer.decode(
|
||||
generated[0], skip_special_tokens=True
|
||||
)
|
||||
print(f"🎯 Результат: '{generated_text}'")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка генерации: {e}")
|
||||
|
||||
|
||||
# === Сохранение результатов ===
|
||||
results = {
|
||||
"experiment": experiment_name,
|
||||
"model_config": model_config,
|
||||
"training_config": TRAINING_CONFIG,
|
||||
"final_loss": train_result.metrics.get('train_loss', 'N/A'),
|
||||
"eval_loss": train_result.metrics.get('eval_loss', 'N/A')
|
||||
"final_loss": train_result.metrics.get("train_loss", "N/A"),
|
||||
"eval_loss": train_result.metrics.get("eval_loss", "N/A"),
|
||||
}
|
||||
|
||||
|
||||
logger.save_logs("checkpoints/hf_integration_training_logs.json")
|
||||
|
||||
|
||||
print(f"\n🎉 Эксперимент с HF интеграцией завершен успешно!")
|
||||
print(f"\n💡 Для использования обученной модели:")
|
||||
print(f" uv run python experiments/hf_integration/generate_with_hf_tools.py")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в эксперименте: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
||||
19
experiments/llm_only/configs/gpt2_generate.json
Normal file
19
experiments/llm_only/configs/gpt2_generate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"test_prompts": [
|
||||
"Нейронные сети",
|
||||
"Обработка естественного языка",
|
||||
"GPT-2 — это"
|
||||
],
|
||||
"model_config_path": "checkpoints/gpt2-bpe/config.json",
|
||||
"model_weights": "checkpoints/gpt2-bpe/model.pt",
|
||||
"generation": {
|
||||
"max_new_tokens": 40,
|
||||
"temperature": 0.8,
|
||||
"do_sample": true,
|
||||
"top_k": null,
|
||||
"top_p": null
|
||||
},
|
||||
"log_path": "checkpoints/llm_only_generation_logs.json"
|
||||
}
|
||||
|
||||
23
experiments/llm_only/configs/gpt2_train.json
Normal file
23
experiments/llm_only/configs/gpt2_train.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"bpe_vocab_size": 1000,
|
||||
"bpe_special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
"test_prompts": ["Искусственный интеллект", "Python — это"],
|
||||
"model_config": {
|
||||
"vocab_size": null,
|
||||
"embed_dim": 256,
|
||||
"num_heads": 4,
|
||||
"num_layers": 4,
|
||||
"max_position_embeddings": 128,
|
||||
"dropout": 0.1
|
||||
},
|
||||
"model_weights": "checkpoints/gpt2-bpe/model.pt",
|
||||
"model_config_path": "checkpoints/gpt2-bpe/config.json",
|
||||
"training": {
|
||||
"learning_rate": 0.0003,
|
||||
"batch_size": 2,
|
||||
"num_epochs": 3,
|
||||
"warmup_steps": 50
|
||||
},
|
||||
"log_path": "checkpoints/gpt2_only_training_logs.json"
|
||||
}
|
||||
19
experiments/llm_only/configs/gpt_generate.json
Normal file
19
experiments/llm_only/configs/gpt_generate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"test_prompts": [
|
||||
"The neural network",
|
||||
"Transformer architecture",
|
||||
"GPT models are"
|
||||
],
|
||||
"model_config_path": "checkpoints/gpt-bpe/config.json",
|
||||
"model_weights": "checkpoints/gpt-bpe/model.pt",
|
||||
"generation": {
|
||||
"max_new_tokens": 40,
|
||||
"temperature": 0.8,
|
||||
"do_sample": true,
|
||||
"top_k": null,
|
||||
"top_p": null
|
||||
},
|
||||
"log_path": "checkpoints/llm_only_generation_logs.json"
|
||||
}
|
||||
|
||||
23
experiments/llm_only/configs/gpt_train.json
Normal file
23
experiments/llm_only/configs/gpt_train.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"bpe_vocab_size": 1000,
|
||||
"bpe_special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
"test_prompts": ["GPT language model", "Machine learning basics"],
|
||||
"model_config": {
|
||||
"vocab_size": null,
|
||||
"embed_dim": 256,
|
||||
"num_heads": 4,
|
||||
"num_layers": 4,
|
||||
"max_position_embeddings": 128,
|
||||
"dropout": 0.1
|
||||
},
|
||||
"model_weights": "checkpoints/gpt-bpe/model.pt",
|
||||
"model_config_path": "checkpoints/gpt-bpe/config.json",
|
||||
"training": {
|
||||
"learning_rate": 0.0003,
|
||||
"batch_size": 2,
|
||||
"num_epochs": 3,
|
||||
"warmup_steps": 50
|
||||
},
|
||||
"log_path": "checkpoints/gpt_only_training_logs.json"
|
||||
}
|
||||
19
experiments/llm_only/configs/llama_generate.json
Normal file
19
experiments/llm_only/configs/llama_generate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"test_prompts": [
|
||||
"Open weights",
|
||||
"The Llama model is",
|
||||
"Efficient transformers"
|
||||
],
|
||||
"model_config_path": "checkpoints/llama-bpe/config.json",
|
||||
"model_weights": "checkpoints/llama-bpe/model.pt",
|
||||
"generation": {
|
||||
"max_new_tokens": 40,
|
||||
"temperature": 0.8,
|
||||
"do_sample": true,
|
||||
"top_k": null,
|
||||
"top_p": null
|
||||
},
|
||||
"log_path": "checkpoints/llm_only_generation_logs.json"
|
||||
}
|
||||
|
||||
23
experiments/llm_only/configs/llama_train.json
Normal file
23
experiments/llm_only/configs/llama_train.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"bpe_vocab_size": 1000,
|
||||
"bpe_special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
"test_prompts": ["Open source AI", "What is Llama?"],
|
||||
"model_config": {
|
||||
"vocab_size": null,
|
||||
"embed_dim": 256,
|
||||
"num_heads": 4,
|
||||
"num_layers": 4,
|
||||
"max_position_embeddings": 128,
|
||||
"dropout": 0.1
|
||||
},
|
||||
"model_weights": "checkpoints/llama-bpe/model.pt",
|
||||
"model_config_path": "checkpoints/llama-bpe/config.json",
|
||||
"training": {
|
||||
"learning_rate": 0.0003,
|
||||
"batch_size": 2,
|
||||
"num_epochs": 3,
|
||||
"warmup_steps": 50
|
||||
},
|
||||
"log_path": "checkpoints/llama_only_training_logs.json"
|
||||
}
|
||||
19
experiments/llm_only/configs/mistral_generate.json
Normal file
19
experiments/llm_only/configs/mistral_generate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"test_prompts": [
|
||||
"Open weights",
|
||||
"The Llama model is",
|
||||
"Efficient transformers"
|
||||
],
|
||||
"model_config_path": "checkpoints/mistral-bpe/config.json",
|
||||
"model_weights": "checkpoints/mistral-bpe/model.pt",
|
||||
"generation": {
|
||||
"max_new_tokens": 40,
|
||||
"temperature": 0.8,
|
||||
"do_sample": true,
|
||||
"top_k": null,
|
||||
"top_p": null
|
||||
},
|
||||
"log_path": "checkpoints/mistral_only_generation_logs.json"
|
||||
}
|
||||
|
||||
26
experiments/llm_only/configs/mistral_train.json
Normal file
26
experiments/llm_only/configs/mistral_train.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"bpe_vocab_size": 1000,
|
||||
"bpe_special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
"test_prompts": ["Open source AI", "What is Llama?"],
|
||||
"model_config": {
|
||||
"vocab_size": null,
|
||||
"embed_dim": 256,
|
||||
"num_q_heads": 4,
|
||||
"num_kv_heads": 2,
|
||||
"head_size": 64,
|
||||
"num_layers": 4,
|
||||
"max_position_embeddings": 512,
|
||||
"window_size": 16,
|
||||
"dropout": 0.1
|
||||
},
|
||||
"model_weights": "checkpoints/mistral-bpe/model.pt",
|
||||
"model_config_path": "checkpoints/mistral-bpe/config.json",
|
||||
"training": {
|
||||
"learning_rate": 0.0003,
|
||||
"batch_size": 2,
|
||||
"num_epochs": 3,
|
||||
"warmup_steps": 50
|
||||
},
|
||||
"log_path": "checkpoints/mistral_only_training_logs.json"
|
||||
}
|
||||
19
experiments/llm_only/configs/mixtral_generate.json
Normal file
19
experiments/llm_only/configs/mixtral_generate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"test_prompts": [
|
||||
"Open weights",
|
||||
"The Llama model is",
|
||||
"Efficient transformers"
|
||||
],
|
||||
"model_config_path": "checkpoints/mixtral-bpe/config.json",
|
||||
"model_weights": "checkpoints/mixtral-bpe/model.pt",
|
||||
"generation": {
|
||||
"max_new_tokens": 40,
|
||||
"temperature": 0.8,
|
||||
"do_sample": true,
|
||||
"top_k": null,
|
||||
"top_p": null
|
||||
},
|
||||
"log_path": "checkpoints/mixtral_only_generation_logs.json"
|
||||
}
|
||||
|
||||
28
experiments/llm_only/configs/mixtral_train.json
Normal file
28
experiments/llm_only/configs/mixtral_train.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||
"bpe_vocab_size": 1000,
|
||||
"bpe_special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
"test_prompts": ["Open source AI", "What is Llama?"],
|
||||
"model_config": {
|
||||
"vocab_size": null,
|
||||
"embed_dim": 256,
|
||||
"num_q_heads": 4,
|
||||
"num_kv_heads": 2,
|
||||
"head_size": 64,
|
||||
"num_layers": 4,
|
||||
"max_position_embeddings": 512,
|
||||
"num_experts": 8,
|
||||
"top_k_experts": 2,
|
||||
"window_size": 16,
|
||||
"dropout": 0.1
|
||||
},
|
||||
"model_weights": "checkpoints/mixtral-bpe/model.pt",
|
||||
"model_config_path": "checkpoints/mixtral-bpe/config.json",
|
||||
"training": {
|
||||
"learning_rate": 0.0003,
|
||||
"batch_size": 2,
|
||||
"num_epochs": 3,
|
||||
"warmup_steps": 50
|
||||
},
|
||||
"log_path": "checkpoints/mixtral_only_training_logs.json"
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Experiment: generate_gpt_bpe.py
|
||||
Description: Генерация текста обученной GPT моделью с BPE токенизатором.
|
||||
Использует только библиотеку llm без зависимостей от HuggingFace.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к shared модулям
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from llm.models.gpt import GPT2
|
||||
from llm.tokenizers import BPETokenizer
|
||||
|
||||
from shared.configs import (
|
||||
BASE_GPT_CONFIG, TEST_PROMPTS, GENERATION_CONFIG, PATHS
|
||||
)
|
||||
from shared.data import (
|
||||
print_experiment_info, ensure_directories, ExperimentLogger
|
||||
)
|
||||
|
||||
|
||||
def load_model_and_tokenizer() -> tuple:
|
||||
"""
|
||||
Загружает обученную модель и токенизатор.
|
||||
|
||||
Returns:
|
||||
tuple: (модель, токенизатор, конфигурация)
|
||||
"""
|
||||
# Проверяем существование файлов
|
||||
if not os.path.exists(PATHS["gpt_bpe_model"]):
|
||||
raise FileNotFoundError(
|
||||
f"Модель не найдена: {PATHS['gpt_bpe_model']}\n"
|
||||
f"Сначала обучите модель: uv run python experiments/llm_only/train_gpt_bpe.py"
|
||||
)
|
||||
|
||||
if not os.path.exists(PATHS["bpe_tokenizer"]):
|
||||
raise FileNotFoundError(
|
||||
f"Токенизатор не найден: {PATHS['bpe_tokenizer']}"
|
||||
)
|
||||
|
||||
# Загружаем конфигурацию модели
|
||||
import json
|
||||
with open(PATHS["gpt_bpe_config"], 'r', encoding='utf-8') as f:
|
||||
model_config = json.load(f)
|
||||
|
||||
# Загружаем токенизатор
|
||||
print("🔧 Загрузка BPE токенизатора...")
|
||||
tokenizer = BPETokenizer.load(PATHS["bpe_tokenizer"])
|
||||
print(f"✅ Токенизатор загружен (vocab_size={tokenizer.get_vocab_size()})")
|
||||
|
||||
# Загружаем модель
|
||||
print("🔧 Загрузка GPT2 модели...")
|
||||
model = GPT2(model_config)
|
||||
model.load_state_dict(torch.load(PATHS["gpt_bpe_model"], map_location='cpu'))
|
||||
model.eval()
|
||||
print("✅ Модель загружена")
|
||||
|
||||
return model, tokenizer, model_config
|
||||
|
||||
|
||||
def generate_text(
|
||||
model: GPT2,
|
||||
tokenizer: BPETokenizer,
|
||||
prompt: str,
|
||||
config: dict
|
||||
) -> str:
|
||||
"""
|
||||
Генерирует текст на основе промпта.
|
||||
|
||||
Args:
|
||||
model: Обученная GPT модель
|
||||
tokenizer: BPE токенизатор
|
||||
prompt: Входной текст
|
||||
config: Конфигурация генерации
|
||||
|
||||
Returns:
|
||||
str: Сгенерированный текст
|
||||
"""
|
||||
print(f"🔤 Промпт: '{prompt}'")
|
||||
print(f"📊 Параметры: max_tokens={config['max_new_tokens']}, "
|
||||
f"temp={config['temperature']}, sample={config['do_sample']}")
|
||||
|
||||
# Кодируем промпт
|
||||
input_ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
||||
|
||||
print(f"🎯 Токены промпта: {input_ids}")
|
||||
print(f"🎯 Токены (текст): {tokenizer.tokenize(prompt)}")
|
||||
print("🔄 Генерация...")
|
||||
|
||||
# Генерируем текст
|
||||
with torch.no_grad():
|
||||
generated_ids = model.generate(
|
||||
x=input_tensor,
|
||||
max_new_tokens=config["max_new_tokens"],
|
||||
do_sample=config["do_sample"],
|
||||
temperature=config["temperature"],
|
||||
top_k=config["top_k"],
|
||||
top_p=config["top_p"]
|
||||
)
|
||||
|
||||
# Декодируем результат
|
||||
generated_text = tokenizer.decode(generated_ids[0].tolist())
|
||||
|
||||
return generated_text
|
||||
|
||||
|
||||
def test_different_strategies(model: GPT2, tokenizer: BPETokenizer, prompt: str):
|
||||
"""
|
||||
Тестирует разные стратегии генерации на одном промпте.
|
||||
|
||||
Args:
|
||||
model: Обученная модель
|
||||
tokenizer: BPE токенизатор
|
||||
prompt: Тестовый промпт
|
||||
"""
|
||||
print(f"\n🎭 Сравнение стратегий генерации для промпта: '{prompt}'")
|
||||
print("=" * 60)
|
||||
|
||||
strategies = [
|
||||
{"name": "🎯 Жадный поиск", "do_sample": False, "temperature": 1.0},
|
||||
{"name": "🎲 Вероятностная (temp=0.7)", "do_sample": True, "temperature": 0.7},
|
||||
{"name": "🔥 Случайная (temp=1.2)", "do_sample": True, "temperature": 1.2},
|
||||
{"name": "❄️ Детерминированная (temp=0.3)", "do_sample": True, "temperature": 0.3},
|
||||
]
|
||||
|
||||
for strategy in strategies:
|
||||
print(f"\n{strategy['name']}:")
|
||||
try:
|
||||
config = GENERATION_CONFIG.copy()
|
||||
config.update({
|
||||
"do_sample": strategy["do_sample"],
|
||||
"temperature": strategy["temperature"],
|
||||
"max_new_tokens": 20
|
||||
})
|
||||
|
||||
generated = generate_text(model, tokenizer, prompt, config)
|
||||
|
||||
# Выделяем сгенерированную часть
|
||||
generated_part = generated[len(prompt):]
|
||||
print(f" 📤 Промпт: '{prompt}'")
|
||||
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f" 📄 Полный текст: '{generated}'")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Ошибка: {e}")
|
||||
|
||||
|
||||
def analyze_tokenization(tokenizer: BPETokenizer, texts: list):
|
||||
"""
|
||||
Анализирует токенизацию различных текстов.
|
||||
|
||||
Args:
|
||||
tokenizer: BPE токенизатор
|
||||
texts: Список текстов для анализа
|
||||
"""
|
||||
print(f"\n🔍 Анализ токенизации BPE:")
|
||||
print("=" * 50)
|
||||
|
||||
for i, text in enumerate(texts):
|
||||
print(f"\nТекст {i+1}: '{text}'")
|
||||
|
||||
# Токенизация
|
||||
tokens = tokenizer.encode(text, add_special_tokens=False)
|
||||
token_strings = tokenizer.tokenize(text)
|
||||
|
||||
print(f" Токены (ID): {tokens}")
|
||||
print(f" Токены (текст): {token_strings}")
|
||||
print(f" Количество токенов: {len(tokens)}")
|
||||
print(f" Эффективность: {len(text)} символов → {len(tokens)} токенов")
|
||||
|
||||
# Декодирование обратно
|
||||
decoded = tokenizer.decode(tokens)
|
||||
if text == decoded:
|
||||
print(f" ✅ Декодирование корректно")
|
||||
else:
|
||||
print(f" ⚠️ Расхождения: '{decoded}'")
|
||||
|
||||
|
||||
def interactive_generation(model: GPT2, tokenizer: BPETokenizer):
|
||||
"""
|
||||
Режим интерактивной генерации.
|
||||
|
||||
Args:
|
||||
model: Обученная модель
|
||||
tokenizer: BPE токенизатор
|
||||
"""
|
||||
print(f"\n💬 Интерактивная генерация (для выхода введите 'exit')")
|
||||
print("-" * 50)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n🔤 Введите промпт: ").strip()
|
||||
|
||||
if user_input.lower() in ['exit', 'quit', 'выход']:
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Запрашиваем параметры
|
||||
try:
|
||||
max_tokens = int(input("📏 Макс. токенов [50]: ") or "50")
|
||||
temperature = float(input("🌡️ Температура [0.7]: ") or "0.7")
|
||||
do_sample_input = input("🎲 Сэмплирование (y/n) [y]: ").lower()
|
||||
do_sample = do_sample_input != 'n'
|
||||
except:
|
||||
max_tokens = 50
|
||||
temperature = 0.7
|
||||
do_sample = True
|
||||
print("⚠️ Использую параметры по умолчанию")
|
||||
|
||||
config = GENERATION_CONFIG.copy()
|
||||
config.update({
|
||||
"max_new_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"do_sample": do_sample
|
||||
})
|
||||
|
||||
generated = generate_text(model, tokenizer, user_input, config)
|
||||
|
||||
generated_part = generated[len(user_input):]
|
||||
print(f"\n🎯 Результат:")
|
||||
print(f" 📤 Промпт: '{user_input}'")
|
||||
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f" 📄 Полный текст: '{generated}'")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Завершение работы...")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Основная функция эксперимента."""
|
||||
# === Настройка эксперимента ===
|
||||
experiment_name = "Генерация текста GPT2 + BPE (только llm)"
|
||||
experiment_config = {
|
||||
"model": "GPT2 с BPE токенизатором",
|
||||
"стратегия": "автономная генерация",
|
||||
"вход": "промпты",
|
||||
"выход": "сгенерированный текст"
|
||||
}
|
||||
|
||||
print_experiment_info(experiment_name, experiment_config)
|
||||
ensure_directories()
|
||||
logger = ExperimentLogger(experiment_name)
|
||||
|
||||
try:
|
||||
# Загружаем модель и токенизатор
|
||||
model, tokenizer, model_config = load_model_and_tokenizer()
|
||||
|
||||
# === Анализ токенизации ===
|
||||
analysis_texts = [
|
||||
"Искусственный интеллект",
|
||||
"Нейронные сети",
|
||||
"Машинное обучение",
|
||||
]
|
||||
analyze_tokenization(tokenizer, analysis_texts)
|
||||
|
||||
# === Генерация с разными промптами ===
|
||||
print(f"\n🎯 Генерация текста с разными промптами")
|
||||
print("=" * 60)
|
||||
|
||||
for i, prompt in enumerate(TEST_PROMPTS):
|
||||
print(f"\n📝 Пример {i+1}/{len(TEST_PROMPTS)}")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
generated = generate_text(model, tokenizer, prompt, GENERATION_CONFIG)
|
||||
|
||||
# Выделяем сгенерированную часть
|
||||
generated_part = generated[len(prompt):]
|
||||
|
||||
print(f"📤 Промпт: '{prompt}'")
|
||||
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f"📄 Полный текст: '{generated}'")
|
||||
print(f"📏 Длина: {len(generated)} символов")
|
||||
|
||||
# Логируем успешную генерацию
|
||||
logger.log_metric(f"generation_length_{i}", len(generated))
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при генерации: {e}")
|
||||
continue
|
||||
|
||||
# === Сравнение стратегий генерации ===
|
||||
test_prompt = "Искусственный"
|
||||
test_different_strategies(model, tokenizer, test_prompt)
|
||||
|
||||
# === Интерактивная генерация ===
|
||||
interactive_generation(model, tokenizer)
|
||||
|
||||
# === Сохранение результатов ===
|
||||
logger.save_logs("checkpoints/llm_only_generation_logs.json")
|
||||
|
||||
print(f"\n🎉 Эксперимент генерации завершен успешно!")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"❌ {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в эксперименте: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,313 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Experiment: generate_gpt_bpe.py
|
||||
Description: Генерация текста обученной GPT моделью с BPE токенизатором.
|
||||
Использует только библиотеку llm без зависимостей от HuggingFace.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к shared модулям
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from llm.models.gpt import GPT
|
||||
from llm.tokenizers import BPETokenizer
|
||||
|
||||
from shared.configs import (
|
||||
BASE_GPT_CONFIG, TEST_PROMPTS, GENERATION_CONFIG, PATHS
|
||||
)
|
||||
from shared.data import (
|
||||
print_experiment_info, ensure_directories, ExperimentLogger
|
||||
)
|
||||
|
||||
|
||||
def load_model_and_tokenizer() -> tuple:
|
||||
"""
|
||||
Загружает обученную модель и токенизатор.
|
||||
|
||||
Returns:
|
||||
tuple: (модель, токенизатор, конфигурация)
|
||||
"""
|
||||
# Проверяем существование файлов
|
||||
if not os.path.exists(PATHS["gpt_bpe_model"]):
|
||||
raise FileNotFoundError(
|
||||
f"Модель не найдена: {PATHS['gpt_bpe_model']}\n"
|
||||
f"Сначала обучите модель: uv run python experiments/llm_only/train_gpt_bpe.py"
|
||||
)
|
||||
|
||||
if not os.path.exists(PATHS["bpe_tokenizer"]):
|
||||
raise FileNotFoundError(
|
||||
f"Токенизатор не найден: {PATHS['bpe_tokenizer']}"
|
||||
)
|
||||
|
||||
# Загружаем конфигурацию модели
|
||||
import json
|
||||
with open(PATHS["gpt_bpe_config"], 'r', encoding='utf-8') as f:
|
||||
model_config = json.load(f)
|
||||
|
||||
# Загружаем токенизатор
|
||||
print("🔧 Загрузка BPE токенизатора...")
|
||||
tokenizer = BPETokenizer.load(PATHS["bpe_tokenizer"])
|
||||
print(f"✅ Токенизатор загружен (vocab_size={tokenizer.get_vocab_size()})")
|
||||
|
||||
# Загружаем модель
|
||||
print("🔧 Загрузка GPT модели...")
|
||||
model = GPT(model_config)
|
||||
model.load_state_dict(torch.load(PATHS["gpt_bpe_model"], map_location='cpu'))
|
||||
model.eval()
|
||||
print("✅ Модель загружена")
|
||||
|
||||
return model, tokenizer, model_config
|
||||
|
||||
|
||||
def generate_text(
|
||||
model: GPT,
|
||||
tokenizer: BPETokenizer,
|
||||
prompt: str,
|
||||
config: dict
|
||||
) -> str:
|
||||
"""
|
||||
Генерирует текст на основе промпта.
|
||||
|
||||
Args:
|
||||
model: Обученная GPT модель
|
||||
tokenizer: BPE токенизатор
|
||||
prompt: Входной текст
|
||||
config: Конфигурация генерации
|
||||
|
||||
Returns:
|
||||
str: Сгенерированный текст
|
||||
"""
|
||||
print(f"🔤 Промпт: '{prompt}'")
|
||||
print(f"📊 Параметры: max_tokens={config['max_new_tokens']}, "
|
||||
f"temp={config['temperature']}, sample={config['do_sample']}")
|
||||
|
||||
# Кодируем промпт
|
||||
input_ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
||||
|
||||
print(f"🎯 Токены промпта: {input_ids}")
|
||||
print(f"🎯 Токены (текст): {tokenizer.tokenize(prompt)}")
|
||||
print("🔄 Генерация...")
|
||||
|
||||
# Генерируем текст
|
||||
with torch.no_grad():
|
||||
generated_ids = model.generate(
|
||||
x=input_tensor,
|
||||
max_new_tokens=config["max_new_tokens"],
|
||||
do_sample=config["do_sample"],
|
||||
temperature=config["temperature"],
|
||||
top_k=config["top_k"],
|
||||
top_p=config["top_p"]
|
||||
)
|
||||
|
||||
# Декодируем результат
|
||||
generated_text = tokenizer.decode(generated_ids[0].tolist())
|
||||
|
||||
return generated_text
|
||||
|
||||
|
||||
def test_different_strategies(model: GPT, tokenizer: BPETokenizer, prompt: str):
|
||||
"""
|
||||
Тестирует разные стратегии генерации на одном промпте.
|
||||
|
||||
Args:
|
||||
model: Обученная модель
|
||||
tokenizer: BPE токенизатор
|
||||
prompt: Тестовый промпт
|
||||
"""
|
||||
print(f"\n🎭 Сравнение стратегий генерации для промпта: '{prompt}'")
|
||||
print("=" * 60)
|
||||
|
||||
strategies = [
|
||||
{"name": "🎯 Жадный поиск", "do_sample": False, "temperature": 1.0},
|
||||
{"name": "🎲 Вероятностная (temp=0.7)", "do_sample": True, "temperature": 0.7},
|
||||
{"name": "🔥 Случайная (temp=1.2)", "do_sample": True, "temperature": 1.2},
|
||||
{"name": "❄️ Детерминированная (temp=0.3)", "do_sample": True, "temperature": 0.3},
|
||||
]
|
||||
|
||||
for strategy in strategies:
|
||||
print(f"\n{strategy['name']}:")
|
||||
try:
|
||||
config = GENERATION_CONFIG.copy()
|
||||
config.update({
|
||||
"do_sample": strategy["do_sample"],
|
||||
"temperature": strategy["temperature"],
|
||||
"max_new_tokens": 20
|
||||
})
|
||||
|
||||
generated = generate_text(model, tokenizer, prompt, config)
|
||||
|
||||
# Выделяем сгенерированную часть
|
||||
generated_part = generated[len(prompt):]
|
||||
print(f" 📤 Промпт: '{prompt}'")
|
||||
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f" 📄 Полный текст: '{generated}'")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Ошибка: {e}")
|
||||
|
||||
|
||||
def analyze_tokenization(tokenizer: BPETokenizer, texts: list):
|
||||
"""
|
||||
Анализирует токенизацию различных текстов.
|
||||
|
||||
Args:
|
||||
tokenizer: BPE токенизатор
|
||||
texts: Список текстов для анализа
|
||||
"""
|
||||
print(f"\n🔍 Анализ токенизации BPE:")
|
||||
print("=" * 50)
|
||||
|
||||
for i, text in enumerate(texts):
|
||||
print(f"\nТекст {i+1}: '{text}'")
|
||||
|
||||
# Токенизация
|
||||
tokens = tokenizer.encode(text, add_special_tokens=False)
|
||||
token_strings = tokenizer.tokenize(text)
|
||||
|
||||
print(f" Токены (ID): {tokens}")
|
||||
print(f" Токены (текст): {token_strings}")
|
||||
print(f" Количество токенов: {len(tokens)}")
|
||||
print(f" Эффективность: {len(text)} символов → {len(tokens)} токенов")
|
||||
|
||||
# Декодирование обратно
|
||||
decoded = tokenizer.decode(tokens)
|
||||
if text == decoded:
|
||||
print(f" ✅ Декодирование корректно")
|
||||
else:
|
||||
print(f" ⚠️ Расхождения: '{decoded}'")
|
||||
|
||||
|
||||
def interactive_generation(model: GPT, tokenizer: BPETokenizer):
|
||||
"""
|
||||
Режим интерактивной генерации.
|
||||
|
||||
Args:
|
||||
model: Обученная модель
|
||||
tokenizer: BPE токенизатор
|
||||
"""
|
||||
print(f"\n💬 Интерактивная генерация (для выхода введите 'exit')")
|
||||
print("-" * 50)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n🔤 Введите промпт: ").strip()
|
||||
|
||||
if user_input.lower() in ['exit', 'quit', 'выход']:
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Запрашиваем параметры
|
||||
try:
|
||||
max_tokens = int(input("📏 Макс. токенов [50]: ") or "50")
|
||||
temperature = float(input("🌡️ Температура [0.7]: ") or "0.7")
|
||||
do_sample_input = input("🎲 Сэмплирование (y/n) [y]: ").lower()
|
||||
do_sample = do_sample_input != 'n'
|
||||
except:
|
||||
max_tokens = 50
|
||||
temperature = 0.7
|
||||
do_sample = True
|
||||
print("⚠️ Использую параметры по умолчанию")
|
||||
|
||||
config = GENERATION_CONFIG.copy()
|
||||
config.update({
|
||||
"max_new_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"do_sample": do_sample
|
||||
})
|
||||
|
||||
generated = generate_text(model, tokenizer, user_input, config)
|
||||
|
||||
generated_part = generated[len(user_input):]
|
||||
print(f"\n🎯 Результат:")
|
||||
print(f" 📤 Промпт: '{user_input}'")
|
||||
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f" 📄 Полный текст: '{generated}'")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Завершение работы...")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Основная функция эксперимента."""
|
||||
# === Настройка эксперимента ===
|
||||
experiment_name = "Генерация текста GPT + BPE (только llm)"
|
||||
experiment_config = {
|
||||
"model": "GPT с BPE токенизатором",
|
||||
"стратегия": "автономная генерация",
|
||||
"вход": "промпты",
|
||||
"выход": "сгенерированный текст"
|
||||
}
|
||||
|
||||
print_experiment_info(experiment_name, experiment_config)
|
||||
ensure_directories()
|
||||
logger = ExperimentLogger(experiment_name)
|
||||
|
||||
try:
|
||||
# Загружаем модель и токенизатор
|
||||
model, tokenizer, model_config = load_model_and_tokenizer()
|
||||
|
||||
# === Анализ токенизации ===
|
||||
analysis_texts = [
|
||||
"Искусственный интеллект",
|
||||
"Нейронные сети",
|
||||
"Машинное обучение",
|
||||
]
|
||||
analyze_tokenization(tokenizer, analysis_texts)
|
||||
|
||||
# === Генерация с разными промптами ===
|
||||
print(f"\n🎯 Генерация текста с разными промптами")
|
||||
print("=" * 60)
|
||||
|
||||
for i, prompt in enumerate(TEST_PROMPTS):
|
||||
print(f"\n📝 Пример {i+1}/{len(TEST_PROMPTS)}")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
generated = generate_text(model, tokenizer, prompt, GENERATION_CONFIG)
|
||||
|
||||
# Выделяем сгенерированную часть
|
||||
generated_part = generated[len(prompt):]
|
||||
|
||||
print(f"📤 Промпт: '{prompt}'")
|
||||
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f"📄 Полный текст: '{generated}'")
|
||||
print(f"📏 Длина: {len(generated)} символов")
|
||||
|
||||
# Логируем успешную генерацию
|
||||
logger.log_metric(f"generation_length_{i}", len(generated))
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при генерации: {e}")
|
||||
continue
|
||||
|
||||
# === Сравнение стратегий генерации ===
|
||||
test_prompt = "Искусственный"
|
||||
test_different_strategies(model, tokenizer, test_prompt)
|
||||
|
||||
# === Интерактивная генерация ===
|
||||
interactive_generation(model, tokenizer)
|
||||
|
||||
# === Сохранение результатов ===
|
||||
logger.save_logs("checkpoints/llm_only_generation_logs.json")
|
||||
|
||||
print(f"\n🎉 Эксперимент генерации завершен успешно!")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"❌ {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в эксперименте: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
173
experiments/llm_only/run_llm_experiment.py
Normal file
173
experiments/llm_only/run_llm_experiment.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Универсальный скрипт для обучения и генерации LLM.
|
||||
Позволяет выбирать тип модели и действие через аргументы,
|
||||
а специальные параметры подавать отдельным JSON-конфигом.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import torch
|
||||
|
||||
# Добавляем директорию shared среди импортируемых
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from llm.tokenizers import BPETokenizer
|
||||
from llm.datasets.text_dataset import TextDataset
|
||||
from llm.training.trainer import Trainer
|
||||
|
||||
from shared.data import (
|
||||
print_experiment_info,
|
||||
ensure_directories,
|
||||
load_training_data,
|
||||
ExperimentLogger,
|
||||
)
|
||||
|
||||
|
||||
def load_config(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_model_class(model_name):
|
||||
if model_name.lower() == 'gpt':
|
||||
from llm.models.gpt import GPT
|
||||
return GPT
|
||||
elif model_name.lower() == 'gpt2':
|
||||
from llm.models.gpt import GPT2
|
||||
return GPT2
|
||||
elif model_name.lower() == 'llama':
|
||||
from llm.models.llama import Llama
|
||||
return Llama
|
||||
elif model_name.lower() == 'mistral':
|
||||
from llm.models.mistral import Mistral
|
||||
return Mistral
|
||||
elif model_name.lower() == 'mixtral':
|
||||
from llm.models.mixtral import Mixtral
|
||||
return Mixtral
|
||||
else:
|
||||
raise ValueError(f"Модель '{model_name}' не поддерживается.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Универсальный запуск обучения/генерации LLM.')
|
||||
parser.add_argument('--model', '-m', type=str, required=True, help='Название модели (gpt, gpt2, llama и т.д.).')
|
||||
parser.add_argument('--action', '-a', type=str, required=True, choices=['train', 'generate'], help='Действие: train или generate.')
|
||||
parser.add_argument('--config', '-c', type=str, required=True, help='Путь к JSON-конфигу с параметрами.')
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config(args.config)
|
||||
ModelClass = load_model_class(args.model)
|
||||
logger = ExperimentLogger(f"{args.action}_{args.model}")
|
||||
|
||||
print_experiment_info(f"Эксперимент {args.action} {args.model}", config)
|
||||
ensure_directories()
|
||||
|
||||
# ==== Обучение ====
|
||||
if args.action == 'train':
|
||||
train_texts, val_texts = load_training_data()
|
||||
# --- Токенизатор ---
|
||||
if os.path.exists(config["bpe_tokenizer"]):
|
||||
print("📝 Загрузка обученного токенизатора...")
|
||||
tokenizer = BPETokenizer.load(config["bpe_tokenizer"])
|
||||
print(f"✅ Токенизатор загружен (vocab_size={tokenizer.get_vocab_size()})")
|
||||
else:
|
||||
print("🔧 Обучение BPE токенизатора...")
|
||||
tokenizer = BPETokenizer()
|
||||
tokenizer.train(
|
||||
texts=train_texts,
|
||||
vocab_size=config["bpe_vocab_size"],
|
||||
special_tokens=config["bpe_special_tokens"]
|
||||
)
|
||||
os.makedirs(os.path.dirname(config["bpe_tokenizer"]), exist_ok=True)
|
||||
tokenizer.save(config["bpe_tokenizer"])
|
||||
print(f"✅ BPE токенизатор обучен и сохранен: {config['bpe_tokenizer']}")
|
||||
|
||||
# Тестируем токенизатор (базово)
|
||||
for test_text in config.get("test_prompts", ["Тест"]):
|
||||
encoded = tokenizer.encode(test_text)
|
||||
decoded = tokenizer.decode(encoded)
|
||||
print(f"[TEST TOK] '{test_text}' → {encoded} → '{decoded}'")
|
||||
|
||||
# --- Модель ---
|
||||
model_config = config["model_config"]
|
||||
model_config["vocab_size"] = tokenizer.get_vocab_size()
|
||||
model = ModelClass(model_config)
|
||||
|
||||
# --- Датасет ---
|
||||
train_dataset = TextDataset(
|
||||
train_texts,
|
||||
tokenizer,
|
||||
block_size=model_config["max_position_embeddings"]
|
||||
)
|
||||
print(f" Размер train датасета: {len(train_dataset)} примеров")
|
||||
|
||||
# --- Trainer ---
|
||||
training = config["training"]
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
train_dataset=train_dataset,
|
||||
lr=training["learning_rate"],
|
||||
batch_size=training["batch_size"],
|
||||
num_epochs=training["num_epochs"],
|
||||
warmup_steps=training.get("warmup_steps", 0),
|
||||
)
|
||||
trainer.train()
|
||||
|
||||
# --- Сохранение модели ---
|
||||
os.makedirs(os.path.dirname(config["model_weights"]), exist_ok=True)
|
||||
torch.save(model.state_dict(), config["model_weights"])
|
||||
with open(config["model_config_path"], "w", encoding="utf-8") as f:
|
||||
json.dump(model_config, f, indent=2, ensure_ascii=False)
|
||||
print(f"✅ Модель сохранена: {config['model_weights']}")
|
||||
|
||||
logger.save_logs(config.get("log_path", "checkpoints/llm_only_training_logs.json"))
|
||||
|
||||
# ==== Генерация ====
|
||||
elif args.action == 'generate':
|
||||
# --- Загрузка ---
|
||||
if not os.path.exists(config["model_weights"]):
|
||||
raise FileNotFoundError(f"Модель не найдена: {config['model_weights']}")
|
||||
if not os.path.exists(config["bpe_tokenizer"]):
|
||||
raise FileNotFoundError(f"Токенизатор не найден: {config['bpe_tokenizer']}")
|
||||
with open(config["model_config_path"], "r", encoding="utf-8") as f:
|
||||
model_config = json.load(f)
|
||||
tokenizer = BPETokenizer.load(config["bpe_tokenizer"])
|
||||
model = ModelClass(model_config)
|
||||
model.load_state_dict(torch.load(config["model_weights"], map_location="cpu"))
|
||||
model.eval()
|
||||
|
||||
def generate(prompt, gen_cfg):
|
||||
print(f"Промпт: {prompt}")
|
||||
input_ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
||||
with torch.no_grad():
|
||||
generated_ids = model.generate(
|
||||
x=input_tensor,
|
||||
max_new_tokens=gen_cfg["max_new_tokens"],
|
||||
do_sample=gen_cfg["do_sample"],
|
||||
temperature=gen_cfg["temperature"],
|
||||
top_k=gen_cfg.get("top_k"),
|
||||
top_p=gen_cfg.get("top_p"),
|
||||
)
|
||||
return tokenizer.decode(generated_ids[0].tolist())
|
||||
|
||||
prompts = config.get("test_prompts", ["Тестовый промпт"])
|
||||
gen_cfg = config.get("generation", {
|
||||
"max_new_tokens": 50,
|
||||
"temperature": 0.7,
|
||||
"do_sample": True,
|
||||
"top_k": None,
|
||||
"top_p": None
|
||||
})
|
||||
for prompt in prompts:
|
||||
generated = generate(prompt, gen_cfg)
|
||||
print(f"\n[RESULT] Prompt: '{prompt}'\n---\n{generated}\n{'='*60}")
|
||||
|
||||
logger.save_logs(config.get("log_path", "checkpoints/llm_only_generation_logs.json"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,231 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Experiment: train_gpt_bpe.py
|
||||
Description: Обучение GPT модели с собственным BPE токенизатором.
|
||||
Использует только библиотеку llm без зависимостей от HuggingFace.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к shared модулям
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from llm.models.gpt import GPT2
|
||||
from llm.tokenizers import BPETokenizer
|
||||
from llm.training.dataset import TextDataset
|
||||
from llm.training.trainer import Trainer
|
||||
|
||||
from shared.configs import (
|
||||
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||
TRAINING_CONFIG, PATHS, TEST_PROMPTS
|
||||
)
|
||||
from shared.data import (
|
||||
load_training_data, ensure_directories,
|
||||
print_experiment_info, ExperimentLogger
|
||||
)
|
||||
|
||||
|
||||
def train_bpe_tokenizer(texts: list, config: dict) -> BPETokenizer:
|
||||
"""
|
||||
Обучает BPE токенизатор на текстах.
|
||||
|
||||
Args:
|
||||
texts: Список текстов для обучения
|
||||
config: Конфигурация токенизатора
|
||||
|
||||
Returns:
|
||||
BPETokenizer: Обученный токенизатор
|
||||
"""
|
||||
print("🔧 Обучение BPE токенизатора...")
|
||||
|
||||
tokenizer = BPETokenizer()
|
||||
tokenizer.train(
|
||||
texts=texts,
|
||||
vocab_size=config["vocab_size"],
|
||||
special_tokens=config["special_tokens"]
|
||||
)
|
||||
|
||||
# Сохраняем токенизатор
|
||||
os.makedirs(os.path.dirname(PATHS["bpe_tokenizer"]), exist_ok=True)
|
||||
tokenizer.save(PATHS["bpe_tokenizer"])
|
||||
|
||||
print(f"✅ BPE токенизатор обучен и сохранен: {PATHS['bpe_tokenizer']}")
|
||||
print(f"📊 Размер словаря: {tokenizer.get_vocab_size()}")
|
||||
|
||||
return tokenizer
|
||||
|
||||
|
||||
def test_tokenizer(tokenizer: BPETokenizer, texts: list):
|
||||
"""
|
||||
Тестирует токенизатор на примерах.
|
||||
|
||||
Args:
|
||||
tokenizer: Обученный токенизатор
|
||||
texts: Список тестовых текстов
|
||||
"""
|
||||
print("\n🧪 Тестирование токенизатора:")
|
||||
|
||||
for i, text in enumerate(texts[:3]):
|
||||
print(f"\nПример {i+1}:")
|
||||
print(f" Исходный текст: '{text}'")
|
||||
|
||||
# Кодирование
|
||||
tokens = tokenizer.encode(text)
|
||||
token_strings = tokenizer.tokenize(text)
|
||||
|
||||
print(f" Токены (ID): {tokens}")
|
||||
print(f" Токены (текст): {token_strings}")
|
||||
print(f" Количество токенов: {len(tokens)}")
|
||||
|
||||
# Декодирование
|
||||
decoded = tokenizer.decode(tokens)
|
||||
print(f" Декодированный: '{decoded}'")
|
||||
|
||||
if text == decoded:
|
||||
print(" ✅ Кодирование/декодирование корректно")
|
||||
else:
|
||||
print(" ⚠️ Небольшие расхождения")
|
||||
|
||||
|
||||
def main():
|
||||
"""Основная функция эксперимента."""
|
||||
# === Настройка эксперимента ===
|
||||
experiment_name = "Обучение GPT2 с BPE токенизатором (только llm)"
|
||||
experiment_config = {
|
||||
"model": "GPT2",
|
||||
"tokenizer": "BPE",
|
||||
"vocab_size": BPE_CONFIG["vocab_size"],
|
||||
"training_epochs": TRAINING_CONFIG["num_epochs"],
|
||||
"batch_size": TRAINING_CONFIG["batch_size"],
|
||||
"learning_rate": TRAINING_CONFIG["learning_rate"]
|
||||
}
|
||||
|
||||
print_experiment_info(experiment_name, experiment_config)
|
||||
ensure_directories()
|
||||
logger = ExperimentLogger(experiment_name)
|
||||
|
||||
try:
|
||||
# === Подготовка данных ===
|
||||
train_texts, val_texts = load_training_data()
|
||||
print(f"📊 Данные: {len(train_texts)} train, {len(val_texts)} validation")
|
||||
|
||||
# === Обучение токенизатора ===
|
||||
if os.path.exists(PATHS["bpe_tokenizer"]):
|
||||
print("📝 Загрузка предварительно обученного токенизатора...")
|
||||
tokenizer = BPETokenizer.load(PATHS["bpe_tokenizer"])
|
||||
print(f"✅ Токенизатор загружен (vocab_size={tokenizer.get_vocab_size()})")
|
||||
else:
|
||||
tokenizer = train_bpe_tokenizer(TRAIN_TEXTS, BPE_CONFIG)
|
||||
|
||||
# Тестируем токенизатор
|
||||
test_tokenizer(tokenizer, TEST_PROMPTS[:3])
|
||||
|
||||
# === Инициализация модели ===
|
||||
model_config = BASE_GPT_CONFIG.copy()
|
||||
model_config["vocab_size"] = tokenizer.get_vocab_size()
|
||||
|
||||
print(f"\n🔧 Инициализация GPT2 модели...")
|
||||
print(f" Размер словаря: {model_config['vocab_size']}")
|
||||
print(f" Размер эмбеддингов: {model_config['embed_dim']}")
|
||||
print(f" Количество слоев: {model_config['num_layers']}")
|
||||
print(f" Количество голов внимания: {model_config['num_heads']}")
|
||||
|
||||
model = GPT2(model_config)
|
||||
|
||||
# === Подготовка датасета ===
|
||||
print(f"\n📊 Подготовка датасета...")
|
||||
train_dataset = TextDataset(
|
||||
train_texts,
|
||||
tokenizer,
|
||||
block_size=model_config["max_position_embeddings"]
|
||||
)
|
||||
print(f" Размер train датасета: {len(train_dataset)} примеров")
|
||||
|
||||
# === Обучение модели ===
|
||||
print(f"\n🎯 Начало обучения GPT2 модели...")
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
train_dataset=train_dataset,
|
||||
lr=TRAINING_CONFIG["learning_rate"],
|
||||
batch_size=TRAINING_CONFIG["batch_size"],
|
||||
num_epochs=TRAINING_CONFIG["num_epochs"],
|
||||
warmup_steps=TRAINING_CONFIG["warmup_steps"]
|
||||
)
|
||||
|
||||
# Запускаем обучение
|
||||
trainer.train()
|
||||
|
||||
# === Сохранение модели ===
|
||||
print(f"\n💾 Сохранение модели...")
|
||||
os.makedirs(os.path.dirname(PATHS["gpt_bpe_model"]), exist_ok=True)
|
||||
|
||||
# Сохраняем модель
|
||||
torch.save(model.state_dict(), PATHS["gpt_bpe_model"])
|
||||
|
||||
# Сохраняем конфигурацию
|
||||
import json
|
||||
with open(PATHS["gpt_bpe_config"], 'w', encoding='utf-8') as f:
|
||||
json.dump(model_config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✅ Модель сохранена:")
|
||||
print(f" - {PATHS['gpt_bpe_model']}: веса модели")
|
||||
print(f" - {PATHS['gpt_bpe_config']}: конфигурация модели")
|
||||
print(f" - {PATHS['bpe_tokenizer']}: токенизатор")
|
||||
|
||||
# === Тестирование генерации ===
|
||||
print(f"\n🧪 Тестирование генерации текста...")
|
||||
model.eval()
|
||||
|
||||
for prompt in TEST_PROMPTS[:3]:
|
||||
print(f"\n🔤 Промпт: '{prompt}'")
|
||||
|
||||
try:
|
||||
# Кодируем промпт
|
||||
input_ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
||||
|
||||
# Генерируем текст
|
||||
with torch.no_grad():
|
||||
generated_ids = model.generate(
|
||||
x=input_tensor,
|
||||
max_new_tokens=20,
|
||||
do_sample=True,
|
||||
temperature=0.8
|
||||
)
|
||||
|
||||
# Декодируем результат
|
||||
generated_text = tokenizer.decode(generated_ids[0].tolist())
|
||||
generated_part = generated_text[len(prompt):]
|
||||
|
||||
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f"📄 Полный текст: '{generated_text}'")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка генерации: {e}")
|
||||
|
||||
# === Сохранение результатов ===
|
||||
results = {
|
||||
"experiment": experiment_name,
|
||||
"model_config": model_config,
|
||||
"training_config": TRAINING_CONFIG,
|
||||
"tokenizer_vocab_size": tokenizer.get_vocab_size(),
|
||||
"final_loss": "см. логи обучения" # В реальном эксперименте можно сохранить final loss
|
||||
}
|
||||
|
||||
logger.save_logs("checkpoints/llm_only_training_logs.json")
|
||||
|
||||
print(f"\n🎉 Эксперимент завершен успешно!")
|
||||
print(f"\n💡 Для использования обученной модели:")
|
||||
print(f" uv run python experiments/llm_only/generate_gpt_bpe.py")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в эксперименте: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,231 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Experiment: train_gpt_bpe.py
|
||||
Description: Обучение GPT модели с собственным BPE токенизатором.
|
||||
Использует только библиотеку llm без зависимостей от HuggingFace.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к shared модулям
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from llm.models.gpt import GPT
|
||||
from llm.tokenizers import BPETokenizer
|
||||
from llm.training.dataset import TextDataset
|
||||
from llm.training.trainer import Trainer
|
||||
|
||||
from shared.configs import (
|
||||
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||
TRAINING_CONFIG, PATHS, TEST_PROMPTS
|
||||
)
|
||||
from shared.data import (
|
||||
load_training_data, ensure_directories,
|
||||
print_experiment_info, ExperimentLogger
|
||||
)
|
||||
|
||||
|
||||
def train_bpe_tokenizer(texts: list, config: dict) -> BPETokenizer:
|
||||
"""
|
||||
Обучает BPE токенизатор на текстах.
|
||||
|
||||
Args:
|
||||
texts: Список текстов для обучения
|
||||
config: Конфигурация токенизатора
|
||||
|
||||
Returns:
|
||||
BPETokenizer: Обученный токенизатор
|
||||
"""
|
||||
print("🔧 Обучение BPE токенизатора...")
|
||||
|
||||
tokenizer = BPETokenizer()
|
||||
tokenizer.train(
|
||||
texts=texts,
|
||||
vocab_size=config["vocab_size"],
|
||||
special_tokens=config["special_tokens"]
|
||||
)
|
||||
|
||||
# Сохраняем токенизатор
|
||||
os.makedirs(os.path.dirname(PATHS["bpe_tokenizer"]), exist_ok=True)
|
||||
tokenizer.save(PATHS["bpe_tokenizer"])
|
||||
|
||||
print(f"✅ BPE токенизатор обучен и сохранен: {PATHS['bpe_tokenizer']}")
|
||||
print(f"📊 Размер словаря: {tokenizer.get_vocab_size()}")
|
||||
|
||||
return tokenizer
|
||||
|
||||
|
||||
def test_tokenizer(tokenizer: BPETokenizer, texts: list):
|
||||
"""
|
||||
Тестирует токенизатор на примерах.
|
||||
|
||||
Args:
|
||||
tokenizer: Обученный токенизатор
|
||||
texts: Список тестовых текстов
|
||||
"""
|
||||
print("\n🧪 Тестирование токенизатора:")
|
||||
|
||||
for i, text in enumerate(texts[:3]):
|
||||
print(f"\nПример {i+1}:")
|
||||
print(f" Исходный текст: '{text}'")
|
||||
|
||||
# Кодирование
|
||||
tokens = tokenizer.encode(text)
|
||||
token_strings = tokenizer.tokenize(text)
|
||||
|
||||
print(f" Токены (ID): {tokens}")
|
||||
print(f" Токены (текст): {token_strings}")
|
||||
print(f" Количество токенов: {len(tokens)}")
|
||||
|
||||
# Декодирование
|
||||
decoded = tokenizer.decode(tokens)
|
||||
print(f" Декодированный: '{decoded}'")
|
||||
|
||||
if text == decoded:
|
||||
print(" ✅ Кодирование/декодирование корректно")
|
||||
else:
|
||||
print(" ⚠️ Небольшие расхождения")
|
||||
|
||||
|
||||
def main():
|
||||
"""Основная функция эксперимента."""
|
||||
# === Настройка эксперимента ===
|
||||
experiment_name = "Обучение GPT с BPE токенизатором (только llm)"
|
||||
experiment_config = {
|
||||
"model": "GPT",
|
||||
"tokenizer": "BPE",
|
||||
"vocab_size": BPE_CONFIG["vocab_size"],
|
||||
"training_epochs": TRAINING_CONFIG["num_epochs"],
|
||||
"batch_size": TRAINING_CONFIG["batch_size"],
|
||||
"learning_rate": TRAINING_CONFIG["learning_rate"]
|
||||
}
|
||||
|
||||
print_experiment_info(experiment_name, experiment_config)
|
||||
ensure_directories()
|
||||
logger = ExperimentLogger(experiment_name)
|
||||
|
||||
try:
|
||||
# === Подготовка данных ===
|
||||
train_texts, val_texts = load_training_data()
|
||||
print(f"📊 Данные: {len(train_texts)} train, {len(val_texts)} validation")
|
||||
|
||||
# === Обучение токенизатора ===
|
||||
if os.path.exists(PATHS["bpe_tokenizer"]):
|
||||
print("📝 Загрузка предварительно обученного токенизатора...")
|
||||
tokenizer = BPETokenizer.load(PATHS["bpe_tokenizer"])
|
||||
print(f"✅ Токенизатор загружен (vocab_size={tokenizer.get_vocab_size()})")
|
||||
else:
|
||||
tokenizer = train_bpe_tokenizer(TRAIN_TEXTS, BPE_CONFIG)
|
||||
|
||||
# Тестируем токенизатор
|
||||
test_tokenizer(tokenizer, TEST_PROMPTS[:3])
|
||||
|
||||
# === Инициализация модели ===
|
||||
model_config = BASE_GPT_CONFIG.copy()
|
||||
model_config["vocab_size"] = tokenizer.get_vocab_size()
|
||||
|
||||
print(f"\n🔧 Инициализация GPT модели...")
|
||||
print(f" Размер словаря: {model_config['vocab_size']}")
|
||||
print(f" Размер эмбеддингов: {model_config['embed_dim']}")
|
||||
print(f" Количество слоев: {model_config['num_layers']}")
|
||||
print(f" Количество голов внимания: {model_config['num_heads']}")
|
||||
|
||||
model = GPT(model_config)
|
||||
|
||||
# === Подготовка датасета ===
|
||||
print(f"\n📊 Подготовка датасета...")
|
||||
train_dataset = TextDataset(
|
||||
train_texts,
|
||||
tokenizer,
|
||||
block_size=model_config["max_position_embeddings"]
|
||||
)
|
||||
print(f" Размер train датасета: {len(train_dataset)} примеров")
|
||||
|
||||
# === Обучение модели ===
|
||||
print(f"\n🎯 Начало обучения GPT модели...")
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
train_dataset=train_dataset,
|
||||
lr=TRAINING_CONFIG["learning_rate"],
|
||||
batch_size=TRAINING_CONFIG["batch_size"],
|
||||
num_epochs=TRAINING_CONFIG["num_epochs"],
|
||||
warmup_steps=TRAINING_CONFIG["warmup_steps"]
|
||||
)
|
||||
|
||||
# Запускаем обучение
|
||||
trainer.train()
|
||||
|
||||
# === Сохранение модели ===
|
||||
print(f"\n💾 Сохранение модели...")
|
||||
os.makedirs(os.path.dirname(PATHS["gpt_bpe_model"]), exist_ok=True)
|
||||
|
||||
# Сохраняем модель
|
||||
torch.save(model.state_dict(), PATHS["gpt_bpe_model"])
|
||||
|
||||
# Сохраняем конфигурацию
|
||||
import json
|
||||
with open(PATHS["gpt_bpe_config"], 'w', encoding='utf-8') as f:
|
||||
json.dump(model_config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✅ Модель сохранена:")
|
||||
print(f" - {PATHS['gpt_bpe_model']}: веса модели")
|
||||
print(f" - {PATHS['gpt_bpe_config']}: конфигурация модели")
|
||||
print(f" - {PATHS['bpe_tokenizer']}: токенизатор")
|
||||
|
||||
# === Тестирование генерации ===
|
||||
print(f"\n🧪 Тестирование генерации текста...")
|
||||
model.eval()
|
||||
|
||||
for prompt in TEST_PROMPTS[:3]:
|
||||
print(f"\n🔤 Промпт: '{prompt}'")
|
||||
|
||||
try:
|
||||
# Кодируем промпт
|
||||
input_ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
||||
|
||||
# Генерируем текст
|
||||
with torch.no_grad():
|
||||
generated_ids = model.generate(
|
||||
x=input_tensor,
|
||||
max_new_tokens=20,
|
||||
do_sample=True,
|
||||
temperature=0.8
|
||||
)
|
||||
|
||||
# Декодируем результат
|
||||
generated_text = tokenizer.decode(generated_ids[0].tolist())
|
||||
generated_part = generated_text[len(prompt):]
|
||||
|
||||
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||
print(f"📄 Полный текст: '{generated_text}'")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка генерации: {e}")
|
||||
|
||||
# === Сохранение результатов ===
|
||||
results = {
|
||||
"experiment": experiment_name,
|
||||
"model_config": model_config,
|
||||
"training_config": TRAINING_CONFIG,
|
||||
"tokenizer_vocab_size": tokenizer.get_vocab_size(),
|
||||
"final_loss": "см. логи обучения" # В реальном эксперименте можно сохранить final loss
|
||||
}
|
||||
|
||||
logger.save_logs("checkpoints/llm_only_training_logs.json")
|
||||
|
||||
print(f"\n🎉 Эксперимент завершен успешно!")
|
||||
print(f"\n💡 Для использования обученной модели:")
|
||||
print(f" uv run python experiments/llm_only/generate_gpt_bpe.py")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в эксперименте: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,7 +30,7 @@ BASE_GPT_CONFIG = {
|
||||
"num_heads": 4,
|
||||
"num_layers": 4,
|
||||
"max_position_embeddings": 128,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
}
|
||||
|
||||
# Конфигурация для маленькой модели (быстрое тестирование)
|
||||
@@ -40,7 +40,7 @@ SMALL_GPT_CONFIG = {
|
||||
"num_heads": 2,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 64,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
}
|
||||
|
||||
# Конфигурация для большой модели (качественное обучение)
|
||||
@@ -50,13 +50,13 @@ LARGE_GPT_CONFIG = {
|
||||
"num_heads": 8,
|
||||
"num_layers": 6,
|
||||
"max_position_embeddings": 256,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
}
|
||||
|
||||
# === Конфигурации токенизатора ===
|
||||
BPE_CONFIG = {
|
||||
"vocab_size": 1000,
|
||||
"special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
"special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
}
|
||||
|
||||
# === Конфигурации обучения ===
|
||||
@@ -65,7 +65,7 @@ TRAINING_CONFIG = {
|
||||
"batch_size": 2,
|
||||
"num_epochs": 3,
|
||||
"warmup_steps": 50,
|
||||
"gradient_clip": 1.0
|
||||
"gradient_clip": 1.0,
|
||||
}
|
||||
|
||||
# === Конфигурации генерации ===
|
||||
@@ -74,7 +74,7 @@ GENERATION_CONFIG = {
|
||||
"temperature": 0.7,
|
||||
"do_sample": True,
|
||||
"top_k": None,
|
||||
"top_p": None
|
||||
"top_p": None,
|
||||
}
|
||||
|
||||
# === Пути для сохранения ===
|
||||
@@ -84,7 +84,7 @@ PATHS = {
|
||||
"gpt_bpe_config": "checkpoints/gpt-bpe/config.json",
|
||||
"hf_tokenizer": "checkpoints/hf-bpe-tokenizer",
|
||||
"hf_model": "checkpoints/hf-trained",
|
||||
"hf_proxy_model": "checkpoints/hf-trained-proxy"
|
||||
"hf_proxy_model": "checkpoints/hf-trained-proxy",
|
||||
}
|
||||
|
||||
# === Тестовые промпты ===
|
||||
|
||||
@@ -10,17 +10,17 @@ from .configs import TRAIN_TEXTS, PATHS
|
||||
def load_training_data(split_ratio: float = 0.8) -> Tuple[List[str], List[str]]:
|
||||
"""
|
||||
Загружает данные для обучения и разделяет на train/validation.
|
||||
|
||||
|
||||
Args:
|
||||
split_ratio: Доля данных для обучения
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple: (train_texts, val_texts)
|
||||
"""
|
||||
train_size = int(len(TRAIN_TEXTS) * split_ratio)
|
||||
train_data = TRAIN_TEXTS[:train_size]
|
||||
val_data = TRAIN_TEXTS[train_size:]
|
||||
|
||||
|
||||
return train_data, val_data
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@ def ensure_directories():
|
||||
"""Создает необходимые директории если они не существуют."""
|
||||
directories = [
|
||||
"checkpoints",
|
||||
"checkpoints/gpt-bpe",
|
||||
"checkpoints/gpt-bpe",
|
||||
"checkpoints/hf-bpe-tokenizer",
|
||||
"checkpoints/hf-trained",
|
||||
"checkpoints/hf-trained-proxy",
|
||||
"logs"
|
||||
"logs",
|
||||
]
|
||||
|
||||
|
||||
for directory in directories:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
@@ -42,33 +42,34 @@ def ensure_directories():
|
||||
def get_model_paths(experiment_type: str = "llm_only") -> dict:
|
||||
"""
|
||||
Возвращает пути для конкретного типа эксперимента.
|
||||
|
||||
|
||||
Args:
|
||||
experiment_type: Тип эксперимента ('llm_only' или 'hf_integration')
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Словарь с путями
|
||||
"""
|
||||
base_paths = PATHS.copy()
|
||||
|
||||
|
||||
if experiment_type == "hf_integration":
|
||||
base_paths.update({
|
||||
"model": base_paths["hf_model"],
|
||||
"tokenizer": base_paths["hf_tokenizer"]
|
||||
})
|
||||
base_paths.update(
|
||||
{"model": base_paths["hf_model"], "tokenizer": base_paths["hf_tokenizer"]}
|
||||
)
|
||||
else: # llm_only
|
||||
base_paths.update({
|
||||
"model": base_paths["gpt_bpe_model"],
|
||||
"tokenizer": base_paths["bpe_tokenizer"]
|
||||
})
|
||||
|
||||
base_paths.update(
|
||||
{
|
||||
"model": base_paths["gpt_bpe_model"],
|
||||
"tokenizer": base_paths["bpe_tokenizer"],
|
||||
}
|
||||
)
|
||||
|
||||
return base_paths
|
||||
|
||||
|
||||
def print_experiment_info(experiment_name: str, config: dict):
|
||||
"""
|
||||
Выводит информацию о запускаемом эксперименте.
|
||||
|
||||
|
||||
Args:
|
||||
experiment_name: Название эксперимента
|
||||
config: Конфигурация эксперимента
|
||||
@@ -85,35 +86,35 @@ def print_experiment_info(experiment_name: str, config: dict):
|
||||
def save_experiment_results(results: dict, filepath: str):
|
||||
"""
|
||||
Сохраняет результаты эксперимента в файл.
|
||||
|
||||
|
||||
Args:
|
||||
results: Словарь с результатами
|
||||
filepath: Путь для сохранения
|
||||
"""
|
||||
import json
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
print(f"✅ Результаты эксперимента сохранены: {filepath}")
|
||||
|
||||
|
||||
def load_experiment_results(filepath: str) -> dict:
|
||||
"""
|
||||
Загружает результаты эксперимента из файла.
|
||||
|
||||
|
||||
Args:
|
||||
filepath: Путь к файлу с результатами
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Загруженные результаты
|
||||
"""
|
||||
import json
|
||||
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
return {}
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@@ -121,42 +122,39 @@ class ExperimentLogger:
|
||||
"""
|
||||
Логгер для экспериментов.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, experiment_name: str):
|
||||
self.experiment_name = experiment_name
|
||||
self.metrics = {}
|
||||
|
||||
|
||||
def log_metric(self, name: str, value: float):
|
||||
"""Логирует метрику."""
|
||||
if name not in self.metrics:
|
||||
self.metrics[name] = []
|
||||
self.metrics[name].append(value)
|
||||
print(f"📈 {name}: {value:.4f}")
|
||||
|
||||
|
||||
def log_step(self, step: int, loss: float, **kwargs):
|
||||
"""Логирует шаг обучения."""
|
||||
print(f"📊 Step {step}: loss={loss:.4f}", end="")
|
||||
for key, value in kwargs.items():
|
||||
print(f", {key}={value:.4f}", end="")
|
||||
print()
|
||||
|
||||
|
||||
def log_epoch(self, epoch: int, train_loss: float, val_loss: float = None):
|
||||
"""Логирует завершение эпохи."""
|
||||
print(f"🎯 Epoch {epoch}: train_loss={train_loss:.4f}", end="")
|
||||
if val_loss is not None:
|
||||
print(f", val_loss={val_loss:.4f}", end="")
|
||||
print()
|
||||
|
||||
|
||||
def save_logs(self, filepath: str):
|
||||
"""Сохраняет логи эксперимента."""
|
||||
import json
|
||||
|
||||
logs = {
|
||||
"experiment_name": self.experiment_name,
|
||||
"metrics": self.metrics
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
|
||||
logs = {"experiment_name": self.experiment_name, "metrics": self.metrics}
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(logs, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
print(f"✅ Логи эксперимента сохранены: {filepath}")
|
||||
|
||||
@@ -27,16 +27,13 @@ __all__ = [
|
||||
# Основные классы адаптера
|
||||
"HFAdapter",
|
||||
"HFGPTAdapter",
|
||||
|
||||
# Конфигурации
|
||||
"HFAdapterConfig",
|
||||
"HFAdapterConfig",
|
||||
"HFPretrainedConfig",
|
||||
|
||||
# Адаптеры токенизаторов
|
||||
"HFTokenizerAdapter",
|
||||
"create_hf_tokenizer",
|
||||
"create_hf_tokenizer",
|
||||
"convert_to_hf_format",
|
||||
|
||||
# Утилиты
|
||||
"HFUtils",
|
||||
"TokenizerWrapper",
|
||||
|
||||
@@ -6,12 +6,12 @@ import torch
|
||||
import torch.nn as nn
|
||||
from typing import Optional, Tuple, Union, List
|
||||
from transformers import (
|
||||
PreTrainedModel,
|
||||
PreTrainedModel,
|
||||
GPT2LMHeadModel,
|
||||
GPT2Config,
|
||||
GenerationConfig,
|
||||
LogitsProcessorList,
|
||||
StoppingCriteriaList
|
||||
StoppingCriteriaList,
|
||||
)
|
||||
from transformers.modeling_outputs import CausalLMOutputWithCrossAttentions
|
||||
|
||||
@@ -24,38 +24,39 @@ 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:
|
||||
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 модели
|
||||
"""
|
||||
@@ -67,7 +68,7 @@ class HFGPTAdapter(PreTrainedModel):
|
||||
"max_position_embeddings": hf_config.max_position_embeddings,
|
||||
"dropout": hf_config.hidden_dropout_prob,
|
||||
}
|
||||
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: Optional[torch.Tensor] = None,
|
||||
@@ -78,11 +79,11 @@ class HFGPTAdapter(PreTrainedModel):
|
||||
output_attentions: Optional[bool] = None,
|
||||
output_hidden_states: Optional[bool] = None,
|
||||
return_dict: Optional[bool] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> Union[Tuple, CausalLMOutputWithCrossAttentions]:
|
||||
"""
|
||||
Прямой проход модели.
|
||||
|
||||
|
||||
Args:
|
||||
input_ids: Входные токены [batch_size, seq_len]
|
||||
attention_mask: Маска внимания [batch_size, seq_len]
|
||||
@@ -92,38 +93,39 @@ class HFGPTAdapter(PreTrainedModel):
|
||||
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
|
||||
|
||||
return_dict = (
|
||||
return_dict if return_dict is not None else self.config.use_return_dict
|
||||
)
|
||||
|
||||
# Основной forward pass
|
||||
outputs = self.llm_model(input_ids)
|
||||
if isinstance(outputs, tuple):
|
||||
logits = outputs[0]
|
||||
else:
|
||||
logits = outputs
|
||||
|
||||
|
||||
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)
|
||||
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,
|
||||
@@ -132,30 +134,27 @@ class HFGPTAdapter(PreTrainedModel):
|
||||
attentions=None,
|
||||
cross_attentions=None,
|
||||
)
|
||||
|
||||
|
||||
def prepare_inputs_for_generation(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
past_key_values: Optional[Tuple] = None,
|
||||
**kwargs
|
||||
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,
|
||||
@@ -163,32 +162,32 @@ class HFGPTAdapter(PreTrainedModel):
|
||||
generation_config: Optional[GenerationConfig] = None,
|
||||
logits_processor: Optional[LogitsProcessorList] = None,
|
||||
stopping_criteria: Optional[StoppingCriteriaList] = None,
|
||||
**kwargs
|
||||
**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)
|
||||
|
||||
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
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -196,64 +195,66 @@ class HFAdapter:
|
||||
"""
|
||||
Основной класс адаптера для преобразования моделей llm в формат HuggingFace.
|
||||
"""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_llm_model(
|
||||
llm_model: GPT,
|
||||
hf_config: Optional[HFAdapterConfig] = None
|
||||
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
|
||||
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')
|
||||
|
||||
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]
|
||||
|
||||
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,
|
||||
@@ -263,21 +264,17 @@ class HFAdapter:
|
||||
"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
|
||||
):
|
||||
def save_pretrained(model: HFGPTAdapter, save_directory: str, **kwargs):
|
||||
"""
|
||||
Сохраняет адаптированную модель в формате HuggingFace.
|
||||
|
||||
|
||||
Args:
|
||||
model: Адаптированная модель
|
||||
save_directory: Директория для сохранения
|
||||
@@ -285,19 +282,19 @@ class HFAdapter:
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
if hasattr(kwargs, "tokenizer") and kwargs["tokenizer"] is not None:
|
||||
kwargs["tokenizer"].save_pretrained(save_directory)
|
||||
|
||||
@@ -6,11 +6,12 @@ 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: Размер словаря
|
||||
@@ -28,6 +29,7 @@ class HFAdapterConfig:
|
||||
eos_token_id: ID токена конца строки
|
||||
bos_token_id: ID токена начала строки
|
||||
"""
|
||||
|
||||
model_type: str = "gpt"
|
||||
vocab_size: int = 50257
|
||||
hidden_size: int = 768
|
||||
@@ -43,49 +45,50 @@ class HFAdapterConfig:
|
||||
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)
|
||||
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_layers": "num_hidden_layers",
|
||||
"num_heads": "num_attention_heads",
|
||||
"max_position_embeddings": "max_position_embeddings",
|
||||
"dropout": "hidden_dropout_prob",
|
||||
"vocab_size": "vocab_size"
|
||||
"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)
|
||||
|
||||
|
||||
@@ -94,8 +97,9 @@ class HFPretrainedConfig(PretrainedConfig):
|
||||
Конфигурация для предобученных моделей HuggingFace.
|
||||
Наследуется от PretrainedConfig для полной совместимости.
|
||||
"""
|
||||
|
||||
model_type = "gpt"
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size=50257,
|
||||
@@ -112,15 +116,15 @@ class HFPretrainedConfig(PretrainedConfig):
|
||||
pad_token_id=50256,
|
||||
eos_token_id=50256,
|
||||
bos_token_id=50256,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
pad_token_id=pad_token_id,
|
||||
eos_token_id=eos_token_id,
|
||||
bos_token_id=bos_token_id,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
self.vocab_size = vocab_size
|
||||
self.hidden_size = hidden_size
|
||||
self.num_hidden_layers = num_hidden_layers
|
||||
|
||||
@@ -12,84 +12,82 @@ 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>')
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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,
|
||||
# input_ids = self.llm_tokenizer.encode(
|
||||
# text,
|
||||
# add_special_tokens=add_special_tokens
|
||||
#)
|
||||
# )
|
||||
if isinstance(text, str):
|
||||
input_ids = self.llm_tokenizer.encode(
|
||||
text,
|
||||
add_special_tokens=add_special_tokens
|
||||
text, add_special_tokens=add_special_tokens
|
||||
)
|
||||
input_ids = [input_ids] # <-- оборачиваем в batch
|
||||
else:
|
||||
# Список строк, батч-режим!
|
||||
input_ids = [
|
||||
self.llm_tokenizer.encode(
|
||||
t,
|
||||
add_special_tokens=add_special_tokens
|
||||
) for t in text
|
||||
self.llm_tokenizer.encode(t, add_special_tokens=add_special_tokens)
|
||||
for t in text
|
||||
]
|
||||
|
||||
|
||||
# Применяем 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,
|
||||
@@ -99,11 +97,11 @@ class HFTokenizerAdapter:
|
||||
truncation: bool = False,
|
||||
max_length: Optional[int] = None,
|
||||
return_tensors: Optional[str] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> Union[List[int], List[List[int]]]:
|
||||
"""
|
||||
Кодирует текст в последовательность токенов.
|
||||
|
||||
|
||||
Args:
|
||||
text: Входной текст
|
||||
text_pair: Второй текст (для парных задач)
|
||||
@@ -112,84 +110,91 @@ class HFTokenizerAdapter:
|
||||
truncation: Обрезать последовательность
|
||||
max_length: Максимальная длина
|
||||
return_tensors: Возвращать тензоры
|
||||
|
||||
|
||||
Returns:
|
||||
Список токенов или список списков токенов
|
||||
"""
|
||||
# Кодируем основной текст
|
||||
token_ids = self.llm_tokenizer.encode(
|
||||
text,
|
||||
add_special_tokens=add_special_tokens
|
||||
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
|
||||
)
|
||||
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
|
||||
**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):
|
||||
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}
|
||||
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,
|
||||
@@ -202,7 +207,7 @@ class HFTokenizerAdapter:
|
||||
):
|
||||
"""
|
||||
Pad a list of encoded inputs.
|
||||
|
||||
|
||||
Args:
|
||||
encoded_inputs: List of encoded inputs
|
||||
padding: Padding strategy
|
||||
@@ -211,7 +216,7 @@ class HFTokenizerAdapter:
|
||||
return_attention_mask: Return attention mask
|
||||
return_tensors: Return tensors
|
||||
verbose: Verbose mode
|
||||
|
||||
|
||||
Returns:
|
||||
Padded inputs
|
||||
"""
|
||||
@@ -224,47 +229,62 @@ class HFTokenizerAdapter:
|
||||
# Обрабатываем разные типы данных
|
||||
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)
|
||||
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)
|
||||
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'):
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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'):
|
||||
elif hasattr(mask, "shape"):
|
||||
padding_mask = torch.zeros(padding_length, dtype=mask.dtype)
|
||||
item["attention_mask"] = torch.cat([mask, padding_mask])
|
||||
else:
|
||||
@@ -272,44 +292,49 @@ class HFTokenizerAdapter:
|
||||
elif return_attention_mask:
|
||||
if isinstance(input_ids, int):
|
||||
item["attention_mask"] = [1] + [0] * padding_length
|
||||
elif hasattr(input_ids, 'shape'):
|
||||
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])
|
||||
item["attention_mask"] = torch.cat(
|
||||
[attention_mask, padding_mask]
|
||||
)
|
||||
else:
|
||||
item["attention_mask"] = [1] * current_len + [0] * padding_length
|
||||
|
||||
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__,
|
||||
@@ -324,77 +349,81 @@ class HFTokenizerAdapter:
|
||||
"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:
|
||||
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:
|
||||
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")
|
||||
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:
|
||||
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:
|
||||
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}")
|
||||
|
||||
raise ValueError(
|
||||
f"Неподдерживаемый тип токенизатора: {llm_tokenizer_type}"
|
||||
)
|
||||
|
||||
else:
|
||||
# Пытаемся загрузить как файл llm токенизатора
|
||||
try:
|
||||
@@ -409,10 +438,10 @@ class HFTokenizerAdapter:
|
||||
def create_hf_tokenizer(llm_tokenizer: BaseTokenizer) -> HFTokenizerAdapter:
|
||||
"""
|
||||
Создает адаптер HuggingFace для кастомного токенизатора.
|
||||
|
||||
|
||||
Args:
|
||||
llm_tokenizer: Токенизатор из библиотеки llm
|
||||
|
||||
|
||||
Returns:
|
||||
HFTokenizerAdapter: Адаптированный токенизатор
|
||||
"""
|
||||
@@ -422,7 +451,7 @@ def create_hf_tokenizer(llm_tokenizer: BaseTokenizer) -> HFTokenizerAdapter:
|
||||
def convert_to_hf_format(llm_tokenizer: BaseTokenizer, save_directory: str):
|
||||
"""
|
||||
Конвертирует кастомный токенизатор в формат HuggingFace.
|
||||
|
||||
|
||||
Args:
|
||||
llm_tokenizer: Токенизатор из llm
|
||||
save_directory: Директория для сохранения
|
||||
|
||||
@@ -14,55 +14,57 @@ 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"
|
||||
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__):
|
||||
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,
|
||||
@@ -70,11 +72,11 @@ class HFUtils:
|
||||
repo_name: str,
|
||||
organization: Optional[str] = None,
|
||||
private: bool = False,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Загружает модель в HuggingFace Hub.
|
||||
|
||||
|
||||
Args:
|
||||
model: Адаптированная модель
|
||||
tokenizer: Токенизатор
|
||||
@@ -85,23 +87,23 @@ class HFUtils:
|
||||
"""
|
||||
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,
|
||||
@@ -110,46 +112,43 @@ class HFUtils:
|
||||
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"
|
||||
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:
|
||||
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,
|
||||
@@ -159,63 +158,56 @@ class HFUtils:
|
||||
"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)
|
||||
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"
|
||||
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'])
|
||||
|
||||
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'
|
||||
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
|
||||
hf_logits.flatten(), llm_logits.flatten(), dim=0
|
||||
)
|
||||
|
||||
|
||||
return {
|
||||
"kl_divergence": kl_divergence.item(),
|
||||
"cosine_similarity": cosine_similarity.item(),
|
||||
@@ -228,58 +220,52 @@ 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
|
||||
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
|
||||
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 {
|
||||
@@ -290,36 +276,27 @@ class TokenizerWrapper:
|
||||
}
|
||||
|
||||
|
||||
def create_hf_pipeline(
|
||||
llm_model,
|
||||
tokenizer=None,
|
||||
device: str = "auto",
|
||||
**kwargs
|
||||
):
|
||||
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
|
||||
"text-generation", model=hf_model, tokenizer=tokenizer, device=device, **kwargs
|
||||
)
|
||||
|
||||
|
||||
return pipe
|
||||
|
||||
270
llm/README.md
270
llm/README.md
@@ -0,0 +1,270 @@
|
||||
# LLM Framework - Фреймворк для языковых моделей
|
||||
|
||||
Модульная библиотека для создания, обучения и использования больших языковых моделей (LLM) с поддержкой различных архитектур (GPT, LLaMA и др.).
|
||||
|
||||
## 🏗️ Архитектура
|
||||
|
||||
Библиотека построена по модульному принципу с четким разделением ответственности:
|
||||
|
||||
```
|
||||
llm/
|
||||
├── core/ # Базовые компоненты
|
||||
│ ├── base_model.py # Абстрактный базовый класс моделей
|
||||
│ ├── cached_decoder.py # Универсальный декодер с кэшированием
|
||||
│ ├── decoder.py # Базовый декодер
|
||||
│ ├── multi_head_attention.py # Многоголовое внимание
|
||||
│ ├── head_attention.py # Одно-головое внимание
|
||||
│ ├── feed_forward.py # Стандартный FFN слой
|
||||
│ ├── token_embeddings.py # Векторные представления токенов
|
||||
│ ├── positional_embeddings.py # Абсолютные позиционные эмбеддинги
|
||||
│ ├── rope.py # Rotary Positional Embeddings (RoPE)
|
||||
│ ├── rms_norm.py # RMS Normalization
|
||||
│ ├── swi_glu.py # SwiGLU активация
|
||||
│ ├── silu.py # SiLU активация
|
||||
│ └── gelu.py # GELU активация
|
||||
├── models/ # Конкретные реализации моделей
|
||||
│ ├── gpt/ # GPT архитектуры
|
||||
│ │ ├── gpt.py # Базовая GPT
|
||||
│ │ ├── gpt2.py # GPT-2 реализация
|
||||
│ │ └── __init__.py
|
||||
│ ├── llama/ # LLaMA архитектура
|
||||
│ │ ├── llama.py # LLaMA реализация
|
||||
│ │ └── __init__.py
|
||||
│ └── mistral/ # Mistral архитектура
|
||||
│ ├── mistral.py # Mistral реализация
|
||||
│ └── __init__.py
|
||||
├── tokenizers/ # Токенизаторы
|
||||
│ ├── base_tokenizer.py # Базовый интерфейс
|
||||
│ └── bpe_tokenizer.py # BPE токенизатор
|
||||
├── datasets/ # Работа с датасетами
|
||||
│ ├── text_dataset.py # Стандартный датасет
|
||||
│ └── streaming_text_dataset.py # Стриминговый датасет
|
||||
└── training/ # Утилиты обучения
|
||||
├── trainer.py # Тренировочный цикл
|
||||
├── optimizer.py # Оптимизаторы
|
||||
└── scheduler.py # Планировщики обучения
|
||||
```
|
||||
|
||||
## 🧩 Ключевые компоненты
|
||||
|
||||
### BaseModel (`core/base_model.py`)
|
||||
**Абстрактный базовый класс** для всех языковых моделей с единым интерфейсом.
|
||||
|
||||
```python
|
||||
class BaseModel(nn.Module, ABC):
|
||||
@abstractmethod
|
||||
def forward(self, input_ids: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
||||
"""Прямой проход модели."""
|
||||
|
||||
@abstractmethod
|
||||
def generate(self, input_ids: torch.Tensor, max_length: int = 50) -> torch.Tensor:
|
||||
"""Генерация текста."""
|
||||
```
|
||||
|
||||
### CachedDecoder (`core/cached_decoder.py`)
|
||||
**Универсальный декодер** с поддержкой dependency injection и кэширования KV-памяти.
|
||||
|
||||
```python
|
||||
CachedDecoder(
|
||||
feed_forward_layer=FeedForward(...), # или SwiGLU
|
||||
norm_layer=nn.LayerNorm, # или RMSNorm
|
||||
rope=RoPE(...), # опционально
|
||||
# ... другие параметры
|
||||
)
|
||||
```
|
||||
|
||||
### RoPE (`core/rope.py`)
|
||||
**Rotary Positional Embeddings** - ротационные позиционные эмбеддинги.
|
||||
|
||||
**Математическая основа:**
|
||||
```
|
||||
θ_i = base^(-2i/d)
|
||||
q'_m = q_m * cos(mθ_i) + rotate(q_m) * sin(mθ_i)
|
||||
```
|
||||
|
||||
### RMSNorm (`core/rms_norm.py`)
|
||||
**Root Mean Square Normalization** - упрощенная нормализация без среднего.
|
||||
|
||||
**Формула:**
|
||||
```
|
||||
RMSNorm(x) = (x / RMS(x)) * w
|
||||
где RMS(x) = sqrt(mean(x²) + eps)
|
||||
```
|
||||
|
||||
### SwiGLU (`core/swi_glu.py`)
|
||||
**Swish-Gated Linear Unit** - современная активация с gating mechanism.
|
||||
|
||||
**Формула:**
|
||||
```
|
||||
SwiGLU(x) = Swish(xW_g + b_g) ⊙ (xW_u + b_u) * W_d + b_d
|
||||
```
|
||||
|
||||
## 🚀 Примеры использования
|
||||
|
||||
### Создание классической GPT модели
|
||||
```python
|
||||
from llm.models.gpt import GPT
|
||||
|
||||
config = {
|
||||
"vocab_size": 50257,
|
||||
"embed_dim": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"max_position_embeddings": 1024,
|
||||
"dropout": 0.1
|
||||
}
|
||||
|
||||
model = GPT(config)
|
||||
```
|
||||
|
||||
### Создание GPT2 модели
|
||||
```python
|
||||
from llm.models.gpt import GPT2
|
||||
|
||||
config = {
|
||||
"vocab_size": 50257,
|
||||
"embed_dim": 768,
|
||||
"num_heads": 12,
|
||||
"num_layers": 12,
|
||||
"max_position_embeddings": 1024,
|
||||
"dropout": 0.1
|
||||
}
|
||||
|
||||
model = GPT2(config)
|
||||
```
|
||||
|
||||
### Создание LLaMA модели
|
||||
```python
|
||||
from llm.models.llama import Llama
|
||||
from llm.core.swi_glu import SwiGLU
|
||||
from llm.core.rms_norm import RMSNorm
|
||||
|
||||
config = {
|
||||
"vocab_size": 32000,
|
||||
"embed_dim": 4096,
|
||||
"num_heads": 32,
|
||||
"num_layers": 32,
|
||||
"max_position_embeddings": 2048,
|
||||
"dropout": 0.1
|
||||
}
|
||||
|
||||
model = Llama(config)
|
||||
```
|
||||
|
||||
### Генерация текста
|
||||
```python
|
||||
# Прямой проход
|
||||
output = model(input_ids, attention_mask)
|
||||
|
||||
# Генерация текста
|
||||
generated = model.generate(input_ids, max_length=100)
|
||||
```
|
||||
|
||||
## 📊 Входные и выходные данные
|
||||
|
||||
### Входные данные:
|
||||
- `input_ids`: `Tensor[int64]` формы `[batch_size, seq_len]` - индексы токенов
|
||||
- `attention_mask`: `Tensor[bool]` формы `[batch_size, seq_len]` - маска внимания
|
||||
- `cache`: `List[Tuple[Tensor, Tensor]]` - кэш ключей-значений для генерации
|
||||
|
||||
### Выходные данные:
|
||||
- `logits`: `Tensor[float32]` формы `[batch_size, seq_len, vocab_size]` - вероятности токенов
|
||||
- `cache`: `List[Tuple[Tensor, Tensor]]` - обновленный кэш (при использовании)
|
||||
|
||||
## 🏆 Поддерживаемые архитектуры
|
||||
|
||||
### GPT (Original) Особенности
|
||||
- ✅ Многоголовое внимание
|
||||
- ✅ Layer Normalization (после внимания и FFN)
|
||||
- ✅ GELU активация
|
||||
- ✅ Learned positional embeddings
|
||||
- ✅ Базовая архитектура трансформер-декодера
|
||||
|
||||
### GPT-2 Особенности
|
||||
- ✅ Layer Normalization (перед вниманием и FFN)
|
||||
- ✅ GELU активация
|
||||
- ✅ Learned positional embeddings
|
||||
- ✅ Кэширование KV для быстрой генерации
|
||||
- ✅ Улучшенная инициализация слоёв
|
||||
|
||||
### LLaMA Особенности
|
||||
- ✅ Rotary Positional Embeddings (RoPE)
|
||||
- ✅ RMS Normalization вместо LayerNorm
|
||||
- ✅ SwiGLU активация вместо GELU
|
||||
- ✅ Оптимизированная структура декодера
|
||||
- ✅ Эффективное кэширование KV-памяти
|
||||
|
||||
### Mistral Особенности
|
||||
- ✅ Sliding Window Attention (оконное внимание)
|
||||
- ✅ Grouped Query Attention (GQA)
|
||||
- ✅ RoPE
|
||||
- ✅ RMSNorm
|
||||
- ✅ Разделённая архитектура на блоки с эффективным управлением памятью
|
||||
- ✅ Совместимость с HuggingFace через hf-proxy
|
||||
|
||||
## 🤝 Интеграция с HuggingFace и BPE
|
||||
|
||||
- Встроенная поддержка собственных BPE токенизаторов и экспериментальная поддержка токенизаторов через HuggingFace (см. hf-proxy).
|
||||
- hf-proxy — экспериментальный модуль! Совместимость с будущими версиями Transformers не гарантируется; API может меняться.
|
||||
- Допускается загрузка/конвертация моделей в формат HF для использования экосистемы Transformers.
|
||||
- Для запуска моделей с токенизаторами HF используйте `hf-proxy` и соответствующие эксперименты из `experiments/hf_integration/`.
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
Запуск всех тестов:
|
||||
```bash
|
||||
cd llm
|
||||
python -m pytest tests/ -v
|
||||
```
|
||||
|
||||
**Статус тестов:** ✅ 101+ тест, охвачены все основные компоненты (ядро, ядро-токенизация, архитектуры, обучение)
|
||||
|
||||
## 📚 Научные концепции
|
||||
|
||||
### Трансформерная архитектура
|
||||
Основана на механизме **внимания**, позволяющем модели взвешивать важность разных частей входной последовательности.
|
||||
|
||||
**Формула внимания:**
|
||||
```
|
||||
Attention(Q, K, V) = softmax(Q·Kᵀ/√d_k)·V
|
||||
```
|
||||
|
||||
### RoPE (Rotary Positional Embeddings)
|
||||
Инновационный метод кодирования позиционной информации через **вращение векторов** в комплексном пространстве.
|
||||
|
||||
**Преимущества:**
|
||||
- Относительное позиционное кодирование
|
||||
- Лучшая экстраполяция на длинные последовательности
|
||||
- Сохранение нормы векторов
|
||||
|
||||
### RMSNorm vs LayerNorm
|
||||
**RMSNorm** устраняет вычитание среднего, что делает его более стабильным и эффективным при обучении больших моделей.
|
||||
|
||||
### SwiGLU vs GELU
|
||||
**SwiGLU** с gating mechanism показывает лучшую производительность благодаря способности выборочно передавать информацию.
|
||||
|
||||
## 🔧 Настройка и расширение
|
||||
|
||||
Библиотека разработана с учетом **расширяемости**. Для добавления новой архитектуры:
|
||||
|
||||
1. **Наследоваться** от `BaseModel`
|
||||
2. **Реализовать** обязательные методы `forward()` и `generate()`
|
||||
3. **Использовать** модульные компоненты из `core/`
|
||||
4. **Добавить** конфигурацию модели
|
||||
|
||||
### Пример расширения:
|
||||
```python
|
||||
class NewModel(BaseModel):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
# Использование готовых компонентов
|
||||
self.decoder = CachedDecoder(...)
|
||||
|
||||
def forward(self, input_ids, attention_mask=None):
|
||||
# Реализация прямого прохода
|
||||
pass
|
||||
```
|
||||
|
||||
## 📄 Лицензия
|
||||
|
||||
Проект распространяется под MIT License.
|
||||
|
||||
@@ -1,20 +1,76 @@
|
||||
# llm/core/base_model.py
|
||||
"""
|
||||
Базовый абстрактный класс для всех больших языковых моделей (LLM).
|
||||
|
||||
Научная суть:
|
||||
Модели типа LLM строятся по модульному принципу — конкретные GPT, LLaMA и др. должны наследоваться от этого класса и реализовывать базовый набор интерфейсов для совместимости с training loop, генерацией, инференсом и т.д.
|
||||
|
||||
Пользовательский уровень:
|
||||
Базовый интерфейс минимизирует дублирование кода и позволяет быстро добавлять новые архитектуры.
|
||||
|
||||
Использование:
|
||||
class MyModel(BaseModel):
|
||||
...
|
||||
model = MyModel(config)
|
||||
logits = model.forward(input_ids)
|
||||
tokens = model.generate(input_ids)
|
||||
"""
|
||||
import torch.nn as nn
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Tuple
|
||||
import torch
|
||||
|
||||
|
||||
class BaseModel(nn.Module, ABC):
|
||||
"""Базовый класс для всех LLM."""
|
||||
"""
|
||||
Абстрактный класс — стандарт для всех архитектур LLM.
|
||||
|
||||
def __init__(self, config):
|
||||
Научная идея:
|
||||
Реализация унифицированного входа/выхода для поддержки построения и обучения любых современных языковых моделей.
|
||||
|
||||
Args:
|
||||
config (dict): Параметры архитектуры (размерность эмбеддингов, число слоев, heads и т.д.)
|
||||
|
||||
Attributes:
|
||||
config (dict): Конфиг модели
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
"""
|
||||
Инициализация модели.
|
||||
|
||||
Args:
|
||||
config (dict): Настройки архитектуры модели (размеры слоев, типы блоков и т.д.)
|
||||
"""
|
||||
super().__init__()
|
||||
self.config = config
|
||||
|
||||
@abstractmethod
|
||||
def forward(self, input_ids, attention_mask=None):
|
||||
"""Прямой проход модели."""
|
||||
def forward(
|
||||
self, input_ids: torch.Tensor, attention_mask: Optional[torch.Tensor] = None
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Прямой проход — получение логитов для входных токенов.
|
||||
|
||||
Args:
|
||||
input_ids (Tensor[int]): Индексы токенов [batch, seq_len]
|
||||
attention_mask (Optional[Tensor[bool]]): Маска разрешенных позиций (если требуется) [batch, seq_len]
|
||||
Returns:
|
||||
logits (Tensor[float]): Логиты словаря [batch, seq_len, vocab_size]
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def generate(self, input_ids, max_length=50):
|
||||
"""Генерация текста (greedy или sampling)."""
|
||||
def generate(self, input_ids: torch.Tensor, max_length: int = 50) -> torch.Tensor:
|
||||
"""
|
||||
Генерация текста (авторегрессивно, greedy или sampling).
|
||||
|
||||
Args:
|
||||
input_ids (Tensor[int]): Начальные токены [batch, start_len]
|
||||
max_length (int): Максимальная длина последовательности
|
||||
Returns:
|
||||
output_tokens (Tensor[int]): Сгенерированная последовательность [batch, generated_len]
|
||||
Пример:
|
||||
>>> logits = model.forward(input_ids)
|
||||
>>> generated = model.generate(input_ids, max_length=128)
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -4,32 +4,94 @@ import torch
|
||||
from torch import nn
|
||||
from .feed_forward import FeedForward
|
||||
from .multi_head_attention import MultiHeadAttention
|
||||
from .rope import RoPE
|
||||
|
||||
|
||||
class CachedDecoder(nn.Module):
|
||||
"""
|
||||
Универсальный декодер с поддержкой кэша для autoregressive использования (GPT, LLAMA и пр).
|
||||
- Поддерживает использование past_key_values для быстрого генеративного инференса.
|
||||
CachedDecoder — Transformer-декодер с key/value-кэшированием (реализация накладывающегося masked multi-head attention).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
Позволяет быстро и эффективно реализовывать autoregressive генерацию текста в стиле GPT-2/3/4:
|
||||
- На шаге генерации используются только нужные токены, “прошлые” key/value значения не пересчитываются, а подаются из кэша.
|
||||
- Позволяет значительно ускорять inferece (особенно на длинных последовательностях).
|
||||
- Вдохновлено реализациями в HuggingFace transformers, GPT-2/3 и других LLM.
|
||||
|
||||
Архитектурные особенности:
|
||||
--------------------------
|
||||
- Использует классическую multi-head attention (с causal mask — запрещает видеть “будущее”).
|
||||
- Предусматривает передачу и накопление KV-cache для каждого слоя (hidden state attention).
|
||||
- Поддерживает передачу внимания через стек attention-блоков.
|
||||
- Применяется layernorm и feed-forward block (GELU).
|
||||
|
||||
Параметры конструктора:
|
||||
-----------------------
|
||||
num_heads : int — число attention heads
|
||||
emb_size : int — embedding размерность
|
||||
head_size : int — размер каждой attention head (обычно emb_size // num_heads)
|
||||
feed_forward_layer : nn.Module — feedforward блок (mLP), может быть любым PyTorch-слоем
|
||||
max_seq_len : int — максимально допустимая длина последовательности
|
||||
dropout : float — dropout на attention/ffn
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> from llm.core.feed_forward import FeedForward
|
||||
>>> ff_block = FeedForward(emb_size=256, dropout=0.1, activation=\"gelu\")
|
||||
>>> decoder = CachedDecoder(num_heads=4, emb_size=256, head_size=64, feed_forward_layer=ff_block, max_seq_len=2048, dropout=0.1)
|
||||
>>> x = torch.randn(2, 100, 256)
|
||||
>>> y, kv_cache = decoder(x, use_cache=True, cache=None)
|
||||
>>> print(y.shape) # torch.Size([2, 100, 256])
|
||||
|
||||
Подробнее:
|
||||
----------
|
||||
- GPT-2: https://cdn.openai.com/better-language-models/language-models.pdf
|
||||
- HuggingFace cache mechanics: https://huggingface.co/docs/transformers/main/en/model_doc/gpt2
|
||||
- Объяснения autoregressive cache: https://jalammar.github.io/illustrated-gpt2/
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
feed_forward_layer: nn.Module,
|
||||
num_heads: int,
|
||||
emb_size: int,
|
||||
head_size: int,
|
||||
max_seq_len: int,
|
||||
norm_layer: type = nn.LayerNorm,
|
||||
dropout: float = 0.1,
|
||||
activation: str = "gelu",
|
||||
rope: RoPE = None,
|
||||
):
|
||||
"""
|
||||
Конструктор CachedDecoder.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
num_heads : int
|
||||
Сколько attention heads используется в каждом attention слое.
|
||||
emb_size : int
|
||||
Размерность входного вектора x.
|
||||
head_size : int
|
||||
Размерность каждой attention head; emb_size = num_heads * head_size должно быть True!
|
||||
feed_forward_layer : nn.Module
|
||||
Feed-forward слой (например, обычный двухслойный MLP), который применяется после нормы и внимания, и после второй нормы.
|
||||
max_seq_len : int
|
||||
Максимальная поддерживаемая длина последовательности (выделяет буфер для causal-маски).
|
||||
dropout : float, default=0.1
|
||||
Dropout после внимания и/или feedforward.
|
||||
"""
|
||||
super().__init__()
|
||||
self._heads = MultiHeadAttention(
|
||||
num_heads=num_heads,
|
||||
emb_size=emb_size,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
rope=rope,
|
||||
dropout=dropout,
|
||||
)
|
||||
self._ff = FeedForward(emb_size=emb_size, dropout=dropout, activation=activation)
|
||||
self._norm1 = nn.LayerNorm(emb_size)
|
||||
self._norm2 = nn.LayerNorm(emb_size)
|
||||
self._ff = feed_forward_layer
|
||||
self._norm1 = norm_layer(emb_size)
|
||||
self._norm2 = norm_layer(emb_size)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
@@ -39,11 +101,30 @@ class CachedDecoder(nn.Module):
|
||||
cache: list = None,
|
||||
):
|
||||
"""
|
||||
x: [batch, seq_len, emb_size]
|
||||
mask: (optional)
|
||||
use_cache: использовать ли кэширование KV-слоев (инкрементальный генератив, GPT-style)
|
||||
cache: список кэшей для голов (или None)
|
||||
Возвращает: (output, new_cache) если use_cache=True, иначе (output, None)
|
||||
Прямой проход через Decoder Block с поддержкой KV-кэша.
|
||||
|
||||
В этом методе применяется:
|
||||
- Causal multi-head attention (masked, не смотрит вперёд)
|
||||
- Быстрая обработка длинных последовательностей за счёт сохранения и передачи KV-кэша
|
||||
- LayerNorm перед каждым блоком
|
||||
- Feed-forward блок и вторая LayerNorm
|
||||
- Dropout
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Вход [batch, seq_len, emb_size]
|
||||
use_cache : bool, по умолчанию True
|
||||
Включать ли накопление и возврат KV-кэша для autoregressive inferece.
|
||||
cache : list, опционально
|
||||
Список предыдущего KV-кеша для attention.
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
x_ff_out : torch.Tensor
|
||||
Результат после attention, модуля и их рез. связей (shape == x)
|
||||
new_cache : new KV-cache (или None)
|
||||
|
||||
"""
|
||||
norm1_out = self._norm1(x)
|
||||
# Передаём все cache/use_cache дальше в attention
|
||||
@@ -58,4 +139,4 @@ class CachedDecoder(nn.Module):
|
||||
if use_cache:
|
||||
return (result, kv_caches)
|
||||
else:
|
||||
return (result, None)
|
||||
return (result, None)
|
||||
|
||||
@@ -3,68 +3,92 @@ import torch
|
||||
from .feed_forward import FeedForward
|
||||
from .multi_head_attention import MultiHeadAttention
|
||||
|
||||
|
||||
class Decoder(nn.Module):
|
||||
"""
|
||||
Декодер трансформера - ключевой компонент архитектуры Transformer.
|
||||
|
||||
Предназначен для:
|
||||
- Обработки последовательностей с учетом контекста (самовнимание)
|
||||
- Постепенного генерирования выходной последовательности
|
||||
- Учета масок для предотвращения "заглядывания в будущее"
|
||||
Decoder — базовый transformer decoder block (pre-LN), классический строительный блок современных языковых моделей.
|
||||
|
||||
Алгоритм работы:
|
||||
1. Входной тензор (batch_size, seq_len, emb_size)
|
||||
2. Многоголовое внимание с residual connection и LayerNorm
|
||||
3. FeedForward сеть с residual connection и LayerNorm
|
||||
4. Выходной тензор (batch_size, seq_len, emb_size)
|
||||
Назначение:
|
||||
-----------
|
||||
- Инкапсулирует архитектуру: norm → multi-head self-attention → residual → norm → feed-forward → residual
|
||||
- Подходит как для LLM/GPT, так и для любых autoregressive sequence моделей.
|
||||
- Использует masked self-attention: каждый токен видит только предыдущие (никакого \"заглядывания в будущее\").
|
||||
- Стабильность обеспечивается через residual connections и LayerNorm после каждого sub-layer.
|
||||
|
||||
Основные характеристики:
|
||||
- Поддержка масок внимания
|
||||
- Residual connections для стабилизации градиентов
|
||||
- Layer Normalization после каждого sub-layer
|
||||
- Конфигурируемые параметры внимания
|
||||
Почему это важно?
|
||||
-----------------
|
||||
- Все современные языковые модели состоят из подобных блоков, соединённых в стек.
|
||||
- Алгоритм residual+norm позволяет проще обучать очень глубокие сети.
|
||||
- Разделение на attention+FFN дает и локальные, и глобальные взаимодействия между токенами.
|
||||
|
||||
Примеры использования:
|
||||
Формула работы (псевдокод):
|
||||
---------------------------
|
||||
y1 = norm1(x)
|
||||
attn_out = Attention(y1)
|
||||
x2 = x + attn_out # residual
|
||||
y2 = norm2(x2)
|
||||
ffn_out = FFN(y2)
|
||||
out = x2 + ffn_out # residual
|
||||
|
||||
1. Базовый случай:
|
||||
>>> decoder = Decoder(num_heads=8, emb_size=512, head_size=64, max_seq_len=1024)
|
||||
>>> x = torch.randn(1, 10, 512) # [batch, seq_len, emb_size]
|
||||
>>> output = decoder(x)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 10, 512])
|
||||
Архитектурные особенности:
|
||||
--------------------------
|
||||
- Поддержка внимания с маской (causal mask или произвольная attention mask)
|
||||
- Residual connections для каждого блока (attention, FFN)
|
||||
- Pre-LN (norm перед каждым подблоком)
|
||||
- Зависит от переданных блоков self_attention и feed_forward, а не их реализации
|
||||
|
||||
2. С маской внимания:
|
||||
>>> mask = torch.tril(torch.ones(10, 10)) # Нижнетреугольная маска
|
||||
>>> output = decoder(x, mask)
|
||||
References:
|
||||
-----------
|
||||
- Vaswani et al., \"Attention is All You Need\" (2017): https://arxiv.org/abs/1706.03762
|
||||
- Illustrated Transformer: https://jalammar.github.io/illustrated-transformer/
|
||||
- Transformer Circuits (дружественное описание): https://transformer-circuits.pub/2021/framework/index.html
|
||||
|
||||
3. Инкрементальное декодирование:
|
||||
>>> for i in range(10):
|
||||
>>> output = decoder(x[:, :i+1, :], mask[:i+1, :i+1])
|
||||
Пример:
|
||||
-------
|
||||
>>> decoder = Decoder(num_heads=8, emb_size=512, head_size=64, max_seq_len=1024)
|
||||
>>> x = torch.randn(1, 10, 512)
|
||||
>>> out = decoder(x)
|
||||
>>> print(out.shape) # torch.Size([1, 10, 512])
|
||||
"""
|
||||
def __init__(self,
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
emb_size: int,
|
||||
head_size: int,
|
||||
max_seq_len: int,
|
||||
dropout: float = 0.1
|
||||
dropout: float = 0.1,
|
||||
):
|
||||
"""
|
||||
Инициализация декодера.
|
||||
Инициализация стандартного decoder-блока для Transformer.
|
||||
|
||||
Параметры:
|
||||
num_heads: int - количество голов внимания
|
||||
emb_size: int - размерность эмбеддингов
|
||||
head_size: int - размерность каждой головы внимания
|
||||
max_seq_len: int - максимальная длина последовательности
|
||||
dropout: float (default=0.1) - вероятность dropout
|
||||
Аргументы:
|
||||
----------
|
||||
num_heads: int
|
||||
Количество attention голов (как делить emb_size на heads)
|
||||
emb_size: int
|
||||
Размерность эмбеддингов (и входа и выхода)
|
||||
head_size: int
|
||||
Размерность одной attention-головы (emb_size = num_heads * head_size)
|
||||
max_seq_len: int
|
||||
Максимальная длина последовательности (важно для mask)
|
||||
dropout: float, default=0.1
|
||||
Dropout после внимания и FFN
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Создаёт слой MultiHeadAttention (masked/casual)
|
||||
- Создаёт двухслойный FeedForward (SwiGLU или GELU)
|
||||
- Применяет 2 слоя LayerNorm для стабилизации градиентов
|
||||
- Все блоки реализованы как PyTorch-модули
|
||||
"""
|
||||
super().__init__()
|
||||
self._heads = MultiHeadAttention(
|
||||
num_heads=num_heads,
|
||||
emb_size=emb_size,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
dropout=dropout
|
||||
num_heads=num_heads,
|
||||
emb_size=emb_size,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
dropout=dropout,
|
||||
)
|
||||
self._ff = FeedForward(emb_size=emb_size, dropout=dropout)
|
||||
self._norm1 = nn.LayerNorm(emb_size)
|
||||
@@ -72,25 +96,31 @@ class Decoder(nn.Module):
|
||||
|
||||
def forward(self, x: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
|
||||
"""
|
||||
Прямой проход через декодер.
|
||||
Один прямой проход через Transformer decoder block.
|
||||
|
||||
Вход:
|
||||
x: torch.Tensor - входной тензор [batch_size, seq_len, emb_size]
|
||||
mask: torch.Tensor (optional) - маска внимания [seq_len, seq_len]
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Входной тензор [batch_size, seq_len, emb_size]
|
||||
mask : torch.Tensor, optional
|
||||
Attention/causal mask (по умолчанию None, тогда будет casual mask по длине seq_len)
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor - выходной тензор [batch_size, seq_len, emb_size]
|
||||
-----------
|
||||
out : torch.Tensor
|
||||
Выходной тензор той же формы, что и x
|
||||
|
||||
Алгоритм forward:
|
||||
1. Применяем MultiHeadAttention к входу
|
||||
2. Добавляем residual connection и LayerNorm
|
||||
3. Применяем FeedForward сеть
|
||||
4. Добавляем residual connection и LayerNorm
|
||||
Алгоритм:
|
||||
---------
|
||||
- Применяем attention к нормализованному входу (layernorm)
|
||||
- Добавляем residual-связь (attention + исходный вход)
|
||||
- Применяем FFN к нормализованному результату (layernorm)
|
||||
- Добавляем residual-связь (ffn + предыдущий выход)
|
||||
"""
|
||||
# Self-Attention блок
|
||||
attention, _ = self._heads(x, mask, use_cache=False, cache=None)
|
||||
out = self._norm1(attention + x)
|
||||
|
||||
|
||||
# FeedForward блок
|
||||
ffn_out = self._ff(out)
|
||||
return self._norm2(ffn_out + out)
|
||||
return self._norm2(ffn_out + out)
|
||||
|
||||
@@ -1,60 +1,76 @@
|
||||
from torch import nn
|
||||
import torch
|
||||
import math
|
||||
from .gelu import GELU
|
||||
|
||||
class GELU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.sqrt_2_over_pi = torch.sqrt(torch.tensor(2.0) / math.pi)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return 0.5 * x * (1 + torch.tanh(
|
||||
self.sqrt_2_over_pi * (x + 0.044715 * torch.pow(x, 3))
|
||||
))
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
"""
|
||||
Слой прямой связи (Feed Forward Network) для архитектуры трансформеров.
|
||||
|
||||
Этот слой состоит из двух линейных преобразований с расширением внутренней размерности
|
||||
в 4 раза и механизмом dropout для регуляризации. Между линейными слоями применяется
|
||||
активация ReLU.
|
||||
FeedForward — классический позиционно-независимый блок для Transformer, применяется к каждому токену отдельно.
|
||||
|
||||
Алгоритм работы:
|
||||
1. Входной тензор x (размерность: [batch_size, seq_len, emb_size])
|
||||
2. Линейное преобразование: emb_size -> 4*emb_size
|
||||
3. Активация ReLU
|
||||
4. Линейное преобразование: 4*emb_size -> emb_size
|
||||
5. Применение dropout
|
||||
6. Возврат результата (размерность: [batch_size, seq_len, emb_size])
|
||||
Назначение и роль:
|
||||
------------------
|
||||
- Реализует двухслойную (или более сложную) нейронную сеть, которая обрабатывает каждый токен ПОРЯДОЧНО независимо (по последней измерении).
|
||||
- Дает модели "нелинейную мощность": любой токен может быть переосмыслен вне глобального контекста.
|
||||
- После слоя внимания (MHA) FFN помогает связать смысл локальных (внутри токена) “скрытых” значений.
|
||||
|
||||
Предназначение:
|
||||
- Добавляет нелинейность в архитектуру трансформера
|
||||
- Обеспечивает взаимодействие между различными размерностями эмбеддингов
|
||||
- Работает независимо для каждого токена в последовательности
|
||||
Архитектурные детали:
|
||||
---------------------
|
||||
- Обычно используется блок: (Linear → Activation → Dropout → Linear → Dropout)
|
||||
- В современных LLM обычно в 4 раза расширяют скрытый слой (inner_dim = 4 * emb_size).
|
||||
- Активация часто GELU или SiLU (Swish), иногда SwiGLU, ReGLU, GeGLU (см. PaLM, Llama).
|
||||
|
||||
Формула (обычная версия):
|
||||
-------------------------
|
||||
FFN(x) = Linear2(Dropout(Activation(Linear1(x))))
|
||||
где Linear1: [emb_size → 4*emb_size], Activation: GELU/SiLU, Linear2: [4*emb_size → emb_size]
|
||||
|
||||
Параметры конструктора:
|
||||
-----------------------
|
||||
emb_size: int — размерность входа/выхода токена
|
||||
inner_dim: int (необязательно) — размер скрытого слоя (по умолчанию 4*emb_size)
|
||||
activation: str — тип активации ('gelu', 'silu', 'relu', ...), см. варианты ниже
|
||||
dropout: float — dropout после каждой линейной проекции
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> ffn = FeedForward(emb_size=256, dropout=0.1, activation='gelu')
|
||||
>>> x = torch.randn(2, 32, 256) # [batch, seq_len, emb_size]
|
||||
>>> y = ffn(x)
|
||||
>>> print(y.shape) # torch.Size([2, 32, 256])
|
||||
|
||||
Пояснения:
|
||||
----------
|
||||
- FeedForward не использует позицию токена — это МLP, применяемый к каждому токену независимо.
|
||||
- Длина последовательности и размер батча не имеют значения (broadcast/reshape по [-2, -1]).
|
||||
- Используется во всех декодерах/энкодерах трансформеров.
|
||||
|
||||
Подробнее смотри:
|
||||
-----------------
|
||||
- Vaswani et al., "Attention is All You Need": https://arxiv.org/abs/1706.03762
|
||||
- GELU: https://arxiv.org/abs/1606.08415
|
||||
- SwiGLU (PaLM, Llama): https://arxiv.org/abs/2002.05202
|
||||
|
||||
Примеры использования:
|
||||
|
||||
>>> # Инициализация слоя
|
||||
>>> ff = FeedForward(emb_size=512, dropout=0.1)
|
||||
>>>
|
||||
>>> # Прямой проход
|
||||
>>> x = torch.randn(32, 10, 512) # [batch_size, seq_len, emb_size]
|
||||
>>> output = ff(x)
|
||||
>>> print(output.shape) # torch.Size([32, 10, 512])
|
||||
>>>
|
||||
>>> # Работа с разными типами данных
|
||||
>>> x_double = torch.randn(32, 10, 512, dtype=torch.float64)
|
||||
>>> output_double = ff(x_double)
|
||||
>>> print(output_double.dtype) # torch.float64
|
||||
"""
|
||||
|
||||
def __init__(self, emb_size: int, dropout: float = 0.1, activation: str = "relu"):
|
||||
"""
|
||||
Инициализация слоя Feed Forward Network.
|
||||
|
||||
Args:
|
||||
emb_size: Размерность входных эмбеддингов
|
||||
dropout: Вероятность dropout для регуляризации (по умолчанию: 0.1)
|
||||
Инициализация FeedForward блока для трансформера.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
emb_size: int
|
||||
Размерность входного и выходного эмбеддинга модели.
|
||||
dropout: float, по умолчанию 0.1
|
||||
Dropout после линии и/или активации (уменьшает переобучение).
|
||||
activation: str, по умолчанию 'gelu'
|
||||
Какая нелинейность использовать ('gelu', 'silu', 'relu' и т.д.).
|
||||
inner_dim: int, опционально
|
||||
Размер скрытого слоя (по умолчанию 4 * emb_size, как в оригинальном Transformer).
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Задает структуру: Linear → Activation → Dropout → Linear → Dropout.
|
||||
"""
|
||||
super().__init__()
|
||||
# Первый линейный слой (расширение размерности)
|
||||
@@ -75,24 +91,34 @@ class FeedForward(nn.Module):
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
Прямой проход через слой Feed Forward Network.
|
||||
|
||||
Args:
|
||||
x: Входной тензор размерности [batch_size, seq_len, emb_size]
|
||||
|
||||
Returns:
|
||||
Тензор той же размерности, что и входной
|
||||
Прямой проход через FeedForward блок.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Входной тензор формы [..., emb_size] (используется на каждом токене отдельно!)
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
torch.Tensor — выход такой же формы, как вход (только последняя размерность сохраняется).
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> ffn = FeedForward(emb_size=256)
|
||||
>>> x = torch.randn(8, 16, 256)
|
||||
>>> y = ffn(x)
|
||||
>>> y.shape # [8, 16, 256]
|
||||
"""
|
||||
# Сохраняем dtype входных данных
|
||||
input_dtype = x.dtype
|
||||
|
||||
|
||||
# Приводим веса к нужному типу если необходимо
|
||||
if input_dtype != self._layer1.weight.dtype:
|
||||
self._layer1 = self._layer1.to(dtype=input_dtype)
|
||||
self._layer2 = self._layer2.to(dtype=input_dtype)
|
||||
|
||||
|
||||
# Пропустим тензор x по очереди через все созданные слои
|
||||
x = self._layer1(x)
|
||||
x = self._activation(x)
|
||||
x = self._layer2(x)
|
||||
return self._dropout(x)
|
||||
return self._dropout(x)
|
||||
|
||||
72
llm/src/llm/core/gelu.py
Normal file
72
llm/src/llm/core/gelu.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import torch
|
||||
from torch import nn
|
||||
import math
|
||||
|
||||
class GELU(nn.Module):
|
||||
"""
|
||||
GELU (Gaussian Error Linear Unit) — современная сглаженная функция активации для нейросетей.
|
||||
|
||||
Мотивация и назначение:
|
||||
-----------------------
|
||||
- GELU используется во всех современных трансформерах (BERT, GPT, Llama) вместо ReLU, поскольку лучше передает градиенты и даёт более "мягкое" обучение.
|
||||
- Формирует плавный переход между активированным и неактивированным состоянием, что улучшает устойчивость и общую производительность больших моделей.
|
||||
- Дает возможность обучению «решать», насколько сильно и в каких диапазонах нужно передавать сигнал (в отличие от жёсткого ReLU).
|
||||
|
||||
Математическая формула:
|
||||
-----------------------
|
||||
GELU(x) = 0.5 * x * (1 + tanh( sqrt(2/pi) * (x + 0.044715 * x^3) ))
|
||||
- Статья (Hendrycks & Gimpel, 2016): https://arxiv.org/abs/1606.08415
|
||||
- В PyTorch с версии 1.4+ встроена как torch.nn.functional.gelu и torch.nn.GELU.
|
||||
|
||||
Как это работает:
|
||||
-----------------
|
||||
- Для каждого входного значения x:
|
||||
- x при больших значениях (большие положительные) почти полностью передается дальше.
|
||||
- x при малых (или сильно отрицательных) "заглушается" к нулю.
|
||||
- На промежуточных значениях — плавный переход.
|
||||
- Является аппроксимацией случайного бинома с гауссовским шумом.
|
||||
|
||||
Args:
|
||||
-----
|
||||
Нет learnable параметров — GELU работает одинаково для всех входов.
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> gelu = GELU()
|
||||
>>> x = torch.tensor([-2.0, 0.0, 2.0])
|
||||
>>> print(gelu(x)) # тензор из плавно переходящих значений
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Hendrycks & Gimpel: https://arxiv.org/abs/1606.08415
|
||||
- BERT, GPT-2 papers (везде используется GELU)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.sqrt_2_over_pi = torch.sqrt(torch.tensor(2.0) / math.pi)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Прямой проход через GELU-активацию.
|
||||
|
||||
Args:
|
||||
-----
|
||||
x : torch.Tensor
|
||||
Любой входной тензор.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
torch.Tensor — тензор той же формы, где к каждому элементу применён GELU.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> gelu = GELU()
|
||||
>>> x = torch.linspace(-3, 3, 7)
|
||||
>>> y = gelu(x)
|
||||
"""
|
||||
return (
|
||||
0.5
|
||||
* x
|
||||
* (1 + torch.tanh(self.sqrt_2_over_pi * (x + 0.044715 * torch.pow(x, 3))))
|
||||
)
|
||||
413
llm/src/llm/core/group_query_attention.py
Normal file
413
llm/src/llm/core/group_query_attention.py
Normal file
@@ -0,0 +1,413 @@
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from llm.core.rope import RoPE
|
||||
|
||||
class GroupedQueryAttention(nn.Module):
|
||||
"""
|
||||
Grouped Query Attention (GQA)
|
||||
=============================
|
||||
|
||||
Что такое Grouped Query Attention?
|
||||
----------------------------------
|
||||
Это разновидность многоголового внимания (multi-head), где для Q (query) голов может быть больше, чем для K/V (key/value) голов:
|
||||
вместо стандартного MHA (num_q_heads == num_kv_heads) — меньшее число K/V разделяет информацию для всех Q.
|
||||
Такой подход экономит память и ускоряет инференс, сохраняя высокое качество внимания (используется например в Mistral, Llama-2, GPT-4 и др.).
|
||||
|
||||
Зачем это нужно?
|
||||
----------------
|
||||
- Сокращает количество вычислений и размер KV-кэша в больших LLM.
|
||||
- Позволяет эффективно масштабировать число attention-глав для моделирования сложных связей, не увеличивая размер всех матриц.
|
||||
|
||||
Как работает?
|
||||
-------------
|
||||
1. Q формируется для каждого query-head (их много)
|
||||
2. K и V вычисляется только для меньшего числа KV-heads (обычно в 2-4 раза меньше, чем Q)
|
||||
3. К/V heads дублируются (repeat) так, чтобы на каждую Q-head был свой набор
|
||||
4. Всё внимание (Q,K,V) — стандартное scaled dot-product, только более эффективно и с компрессией
|
||||
|
||||
Поддержка дополнительных фич:
|
||||
-----------------------------
|
||||
- Rotary Position Encoding (RoPE) для Q и K (для относительной позиции)
|
||||
- Sliding-window attention mask (можно ограничить исторический контекст, как в Mistral)
|
||||
- Кэширование Q/K/V (ускоряет генерацию автоагретивно)
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
num_q_heads: int — количество query голов (Q)
|
||||
num_kv_heads: int — количество key/value голов (обычно меньше Q)
|
||||
emb_size: int — embedding размерность
|
||||
head_size: int — размер каждой attention-head
|
||||
max_seq_len: int — максимальная длина последовательности
|
||||
window_size: int — размер sliding window (макс. количество токенов в контексте внимания)
|
||||
rope: RoPE (по желанию) — если задан, то будет применяться RoPE для Q и K
|
||||
dropout: float — dropout после линейной проекции
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> gqa = GroupedQueryAttention(num_q_heads=8, num_kv_heads=2, emb_size=256, head_size=32, max_seq_len=1024, window_size=256)
|
||||
>>> x = torch.randn(2, 128, 256)
|
||||
>>> y, cache = gqa(x)
|
||||
>>> print(y.shape) # torch.Size([2, 128, 256])
|
||||
|
||||
Где прочитать подробнее:
|
||||
------------------------
|
||||
- LlamaV2 (Section 2.3): https://arxiv.org/abs/2307.09288
|
||||
- Mistral: https://arxiv.org/abs/2310.06825
|
||||
- \"Self-attention with linear complexity\" (Vila et al.): https://arxiv.org/abs/2302.05442
|
||||
- Обзор: https://huggingface.co/blog/mistral
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
emb_size: int,
|
||||
head_size: int,
|
||||
max_seq_len: int,
|
||||
window_size: int,
|
||||
rope: RoPE = None,
|
||||
dropout: float = 0.1,
|
||||
):
|
||||
"""
|
||||
Инициализация слоя Grouped Query Attention (GQA).
|
||||
|
||||
Этот конструктор задаёт архитектуру эффективного внимания, где Q-голов может быть больше, чем KV-голов.
|
||||
Это экономит память/вычисления и позволяет реализовать сдвигающееся "окно" внимания (Mistral-style).
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
num_q_heads : int
|
||||
Количество Query attention heads (чаще всего кратно num_kv_heads, напр. 8/2, 12/4).
|
||||
Чем больше — тем богаче контекстное окно каждой позиции.
|
||||
num_kv_heads : int
|
||||
Количество Key/Value attention heads (обычно 2-4, иногда меньше, чем Query).
|
||||
В современных LLM принято уменьшать их число для оптимизации скорости/кэша.
|
||||
emb_size : int
|
||||
Размерность входного embedding (общий размер вектора на токен).
|
||||
head_size : int
|
||||
Размерность одной головы внимания.
|
||||
Требуется: num_q_heads * head_size == emb_size (иначе ошибка).
|
||||
max_seq_len : int
|
||||
Максимальная поддерживаемая длина входной последовательности; определяет размер триангулярной (causal/sliding window) маски.
|
||||
window_size : int
|
||||
Размер "скользящего окна" истории — сколько токенов учитывается при слепом внимании (как у Mistral).
|
||||
Чем меньше значение, тем локальнее работает внимание (и меньше память/время).
|
||||
rope : RoPE, опционально
|
||||
Если задан — применяется Rotary Positional Encoding к Q и K для относительного позиционного кодирования.
|
||||
dropout : float, по умолчанию 0.1
|
||||
Dropout после линейной проекции attention (обычно 0.1, помогает борьбе с переобучением).
|
||||
|
||||
Что создаётся внутри:
|
||||
---------------------
|
||||
- Линейные слои для получения Q, K, V из embedding.
|
||||
- Буфер для causal/sliding window mask (матрица масок в зависимости от window_size и max_seq_len).
|
||||
- Линейный слой для финального преобразования (объединение всех голов и возврат к emb_size).
|
||||
- Dropout перед возвратом.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> attn = GroupedQueryAttention(
|
||||
... num_q_heads=8, num_kv_heads=2, emb_size=256, head_size=32,
|
||||
... max_seq_len=1024, window_size=256, dropout=0.1)
|
||||
"""
|
||||
super().__init__()
|
||||
self._num_heads = num_q_heads
|
||||
self._num_kv_heads = num_kv_heads
|
||||
self._head_size = head_size
|
||||
self._max_seq_len = max_seq_len
|
||||
self._rope = rope
|
||||
self._window_size = window_size
|
||||
|
||||
self._q = nn.Linear(emb_size, self._num_heads * head_size)
|
||||
self._k = nn.Linear(emb_size, num_kv_heads * head_size)
|
||||
self._v = nn.Linear(emb_size, num_kv_heads * head_size)
|
||||
|
||||
# Создание causal маски
|
||||
mask = self._create_sliding_window_mask(max_seq_len, self._window_size)
|
||||
self.register_buffer(
|
||||
"_tril_mask", mask.bool() if hasattr(torch, "bool") else mask.byte()
|
||||
)
|
||||
|
||||
self._layer = nn.Linear(head_size * self._num_heads, emb_size)
|
||||
self._dropout = nn.Dropout(dropout)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
mask: torch.Tensor = None,
|
||||
use_cache: bool = True,
|
||||
cache: list = None,
|
||||
):
|
||||
"""
|
||||
Шаг внимания в режиме Grouped Query Attention —
|
||||
реализует эффективное многооконное внимание с раздельными Q/KV и sliding/casual mask.
|
||||
|
||||
Что происходит в этом методе:
|
||||
-----------------------------
|
||||
- Преобразует входной тензор x (токеновые эмбеддинги) в Q, K, V-матрицы с учётом разного числа голов для Q и KV.
|
||||
- Формирует attention "маску" для sliding window, если нужно ограничить историю.
|
||||
- Применяет RoPE (если задан) к Q и K, вносит позиционную информацию.
|
||||
- При работе с кэшем дополняет ключи и значения предыдущими (ускоряет генерацию).
|
||||
- Повторяет K/V головы для соответствия количеству Q (чтобы на каждую Q-head приходился свой KV).
|
||||
- Считает обычное scaled dot-product внимание, применяет маску (не даёт видеть будущее, как и в autoregressive).
|
||||
- Softmax, смешивание V на основе attention, объединение всех голов.
|
||||
- Dropout и финальное линейное преобразование обратно к emb_size.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Входной тензор размера [batch, seq_len, emb_size]
|
||||
mask : torch.Tensor, по умолчанию None
|
||||
Матричная маска для внимания (можно передать внешнюю или использовать встроенную sliding window mask)
|
||||
use_cache : bool, по умолчанию True
|
||||
Нужно ли использовать/возвращать кэш KV для быстрых автогенераций.
|
||||
cache : list, опционально
|
||||
Ранее сохранённый кэш KV (используется для инференса по одному токену)
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
- output: torch.Tensor формы [batch, seq_len, emb_size]
|
||||
- kv_cache: кэш новых KV (если use_cache=True), иначе None
|
||||
|
||||
Важно:
|
||||
-------
|
||||
- Реализует Mistral-style attention: к каждой Q-head в итоге “приписан” собственный (но потенциально дублированный) KV-head.
|
||||
- Sliding window ограничивает область вижимости в attention (ускоряет генерацию на длинных последовательностях).
|
||||
- Использование RoPE опционально — но необходимо для современных архитектур LLM.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> attn = GroupedQueryAttention(num_q_heads=8, num_kv_heads=2, emb_size=256, head_size=32, max_seq_len=1024, window_size=256)
|
||||
>>> x = torch.randn(2, 128, 256)
|
||||
>>> y, kv_cache = attn(x)
|
||||
>>> print(y.shape) # torch.Size([2, 128, 256])
|
||||
"""
|
||||
batch_size, seq_len, emb_size = x.shape
|
||||
|
||||
if seq_len > self._max_seq_len:
|
||||
raise ValueError(
|
||||
f"Длина последовательности {seq_len} превышает максимум {self._max_seq_len}"
|
||||
)
|
||||
|
||||
# Пропустите тензор x через матрицы Wq, Wk , Wv, чтобы получить матрицы запроса, ключа и значения.
|
||||
k = self._k(x) # [B, T, hs]
|
||||
q = self._q(x) # [B, T, hs]
|
||||
v = self._v(x) # [B, T, hs]
|
||||
|
||||
# Шаг 2: Изменение формы для multi-head
|
||||
# [batch_size, seq_len, num_heads * head_size]
|
||||
# -> [batch_size, seq_len, num_heads, head_size]
|
||||
# Измените форму запроса (query) на batch_size × num_q_heads × seq_len × head_size.
|
||||
q = q.reshape(batch_size, seq_len, self._num_heads, self._head_size)
|
||||
|
||||
# Измените форму ключа (key) и значения (value) на batch_size × num_kv_heads × seq_len × head_size.
|
||||
k = k.reshape(batch_size, seq_len, self._num_kv_heads, self._head_size)
|
||||
v = v.reshape(batch_size, seq_len, self._num_kv_heads, self._head_size)
|
||||
|
||||
|
||||
# 3. Transpose: [B, T, H, hs] -> [B, H, T, hs]
|
||||
q = q.transpose(1, 2)
|
||||
k = k.transpose(1, 2)
|
||||
v = v.transpose(1, 2)
|
||||
|
||||
start_pos = 0
|
||||
if cache is not None:
|
||||
k_cache, v_cache = cache
|
||||
cache_len = k_cache.shape[2]
|
||||
start_pos = cache_len
|
||||
|
||||
# Пропустите матрицы запроса и ключа через экземпляр rope, чтобы выполнить поворот.
|
||||
if self._rope is not None:
|
||||
# Применяем RoPE к Q и K (НЕ к V!)
|
||||
q = self._rope(q, start_pos=start_pos) # [B, T, hs]
|
||||
k = self._rope(k, start_pos=start_pos) # [B, T, hs]
|
||||
|
||||
# Если cache пришел, то объединяем кэш и одну строку из ключа и значения. Это будут новые key и value для последующих вычислений.
|
||||
# 5. Кэширование (для autoregressive generation)
|
||||
if cache is not None:
|
||||
k_cache, v_cache = cache
|
||||
k = torch.cat([k_cache, k], dim=2) # Concat по seq_len (dim=2)
|
||||
v = torch.cat([v_cache, v], dim=2)
|
||||
|
||||
# Если use_cache == True, то сохраните матрицы ключа и значения для кэша (это нужно сделать до дублирования голов).
|
||||
#if use_cache == True:
|
||||
# # Обрезаем до последних window_size токенов
|
||||
# k_to_cache = k[:, :, -self._window_size:, :]
|
||||
# v_to_cache = v[:, :, -self._window_size:, :]
|
||||
# kv_cache = (k_to_cache, v_to_cache)
|
||||
|
||||
# Продублируйте головы в тензорах ключа (key) и значения (value), чтобы получился тензор размера на batch_size × num_q_heads × seq_len × head_size.
|
||||
#k = self._repeat_kv_heads(k, self._num_heads, self._num_kv_heads)
|
||||
#v = self._repeat_kv_heads(v, self._num_heads, self._num_kv_heads)
|
||||
k_expanded = self._repeat_kv_heads(k, self._num_heads, self._num_kv_heads)
|
||||
v_expanded = self._repeat_kv_heads(v, self._num_heads, self._num_kv_heads)
|
||||
|
||||
# Перемножим матрицы запроса и ключа (транспонированную), чтобы вычислить матрицу внимания.
|
||||
# И разделить все значения в матрице внимания на корень из head_size.
|
||||
scores = q @ k_expanded.transpose(-2, -1) / (self._head_size ** 0.5)
|
||||
|
||||
# 8. Применение маски
|
||||
k_seq_len = k_expanded.size(2) # Длина K после concat с кэшем
|
||||
|
||||
if cache is None:
|
||||
# Случай 1: Без кэша - полная квадратная маска
|
||||
# scores: [B, H, seq_len, seq_len]
|
||||
# Применяем маску [:seq_len, :seq_len]
|
||||
scores = scores.masked_fill(
|
||||
~self._tril_mask[:seq_len, :seq_len],
|
||||
float("-inf")
|
||||
)
|
||||
|
||||
# Применить к матрице внимания (построчно) функцию Softmax.
|
||||
weights = F.softmax(scores, dim=-1)
|
||||
|
||||
# Перемножим матрицу внимания и матрицу значения.
|
||||
x_out = weights @ v_expanded # [B, T, hs]
|
||||
|
||||
# Измените форму тензора на batch_size × seq_len × num_heads*head_size.
|
||||
# Transpose обратно и concatenate heads
|
||||
x_out = x_out.transpose(1, 2) # [B, T_q, H, hs]
|
||||
x_out = x_out.contiguous() # Важно для reshape!
|
||||
concatenated_attention = x_out.reshape(batch_size, seq_len, self._num_heads * self._head_size)
|
||||
|
||||
#concatenated_attention = x_out.reshape(batch_size, seq_len, self._num_heads * self._head_size)
|
||||
|
||||
# Пропустите получившийся тензор через последний линейный слой.
|
||||
# 3. Проецируем в пространство эмбеддингов
|
||||
projected_output = self._layer(concatenated_attention)
|
||||
|
||||
# 4. Применяем dropout для регуляризации
|
||||
output = self._dropout(projected_output)
|
||||
|
||||
if use_cache:
|
||||
# Обрезаем оригинальный K и V (до дублирования)
|
||||
k_to_cache = k[:, :, -self._window_size:, :]
|
||||
v_to_cache = v[:, :, -self._window_size:, :]
|
||||
kv_cache = (k_to_cache, v_to_cache)
|
||||
return output, kv_cache
|
||||
else:
|
||||
return output, None
|
||||
|
||||
def _repeat_kv_heads(
|
||||
self,
|
||||
kv: torch.Tensor,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Приводит число голов K/V к числу голов Q путём поэлементного повторения (tile) KV-голов.
|
||||
|
||||
Зачем это нужно?
|
||||
----------------
|
||||
В Grouped Query Attention (Mistral, Llama-2, GPT-4 и др.) обычно num_kv_heads < num_q_heads.
|
||||
Чтобы каждая Query-head могла смотреть на свою собственную (пусть и общую) KV, мы "нарезаем" или повторяем KV столько раз, сколько требуется — это экономит память и ускоряет генерацию.
|
||||
|
||||
Алгоритм:
|
||||
---------
|
||||
- kv имеет форму [batch_size, num_kv_heads, seq_len, head_size]
|
||||
- Для каждого KV-head делается n_repeat = num_q_heads // num_kv_heads по head-axis (обычно целое)
|
||||
- На выходе форма [batch_size, num_q_heads, seq_len, head_size], где каждый KV-head дублирован для нужного количества Q-heads.
|
||||
|
||||
Args:
|
||||
-----
|
||||
kv : torch.Tensor
|
||||
Входной тензор KV (обычно после linear layer on эмбеддинги), размер [batch_size, num_kv_heads, seq_len, head_size]
|
||||
num_q_heads : int
|
||||
Сколько должно быть Q-голов (их больше!)
|
||||
num_kv_heads : int
|
||||
Сколько KV-голов было (их меньше!)
|
||||
|
||||
Returns:
|
||||
--------
|
||||
torch.Tensor формы [batch_size, num_q_heads, seq_len, head_size], где KV-головы повторены как требуется.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
num_q_heads = 8, num_kv_heads = 2
|
||||
[KV0, KV1] -> [KV0, KV0, KV0, KV0, KV1, KV1, KV1, KV1]
|
||||
# Каждый KV-head дублируется 4 раза, чтобы покрыть все 8 Q-heads.
|
||||
"""
|
||||
batch_size, num_kv_heads, seq_len, head_size = kv.shape
|
||||
|
||||
if num_q_heads == num_kv_heads:
|
||||
# Нет необходимости дублировать
|
||||
return kv
|
||||
|
||||
# Вычисляем сколько раз нужно повторить каждую голову
|
||||
num_repeats = num_q_heads // num_kv_heads
|
||||
|
||||
# repeat_interleave дублирует каждую голову num_repeats раз
|
||||
# [B, num_kv_heads, S, hs] -> [B, num_q_heads, S, hs]
|
||||
# [B, num_kv_heads, S, hs] -> [B, num_kv_heads, 1, S, hs]
|
||||
kv = kv.unsqueeze(2)
|
||||
|
||||
# [B, num_kv_heads, 1, S, hs] -> [B, num_kv_heads, num_repeats, S, hs]
|
||||
kv = kv.repeat(1, 1, num_repeats, 1, 1)
|
||||
|
||||
# [B, num_kv_heads, num_repeats, S, hs] -> [B, num_q_heads, S, hs]
|
||||
kv = kv.reshape(batch_size, num_q_heads, seq_len, head_size)
|
||||
|
||||
|
||||
return kv
|
||||
|
||||
def _create_sliding_window_mask(
|
||||
self,
|
||||
max_seq_len: int,
|
||||
window_size: int,
|
||||
device: torch.device = None
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Создаёт маску для Sliding Window Attention (ограниченного окна внимания).
|
||||
|
||||
Зачем нужна эта маска?
|
||||
----------------------
|
||||
В современных LLM (например, Mistral) self-attention работает не по всей истории, а только в узком "скользящем окне":
|
||||
каждый токен видит только предшествующие (или соседние) токены на расстоянии window_size.
|
||||
Это ускоряет инференс на длинных текстах и экономит память, но сохраняет ключевые зависимости в пределах окна.
|
||||
|
||||
Как работает алгоритм:
|
||||
----------------------
|
||||
- Для каждого токена mask[i, j] == True только если токен j находится СЛЕВА и не дальше, чем window_size позиций (или сам i).
|
||||
- Главное: mask всегда "нижнетреугольная" (causal), плюс полоса шириной window_size вдоль главной диагонали.
|
||||
- Всё за пределами окна — False (attention нельзя).
|
||||
|
||||
Args:
|
||||
-----
|
||||
max_seq_len : int
|
||||
Максимальная длина последовательности (размер будущей attention-матрицы).
|
||||
window_size : int
|
||||
Сколько предыдущих токенов доступно для внимания у каждого шага (вкл. сам себя).
|
||||
device : torch.device, опционально
|
||||
На каком устройстве (cpu/gpu) создавать маску.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
torch.Tensor
|
||||
Маска внимания формы [max_seq_len, max_seq_len], где True — допускается внимание (иначе False).
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> mask = create_sliding_window_mask(8, 3)
|
||||
>>> print(mask.int())
|
||||
tensor([[1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 1, 0, 0, 0, 0, 0, 0],
|
||||
[1, 1, 1, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 0, 0, 0, 0],
|
||||
[0, 0, 1, 1, 1, 0, 0, 0],
|
||||
[0, 0, 0, 1, 1, 1, 0, 0],
|
||||
[0, 0, 0, 0, 1, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 1]])
|
||||
"""
|
||||
row_indices = torch.arange(max_seq_len, device=device).unsqueeze(1) # [max_seq_len, 1]
|
||||
col_indices = torch.arange(max_seq_len, device=device).unsqueeze(0) # [1, max_seq_len]
|
||||
|
||||
causal_mask = col_indices <= row_indices
|
||||
|
||||
window_mask = (row_indices - col_indices) <= window_size
|
||||
|
||||
mask = causal_mask & window_mask
|
||||
|
||||
return mask
|
||||
@@ -1,92 +0,0 @@
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
from math import sqrt
|
||||
|
||||
class HeadAttention(nn.Module):
|
||||
"""
|
||||
Реализация одного головного механизма внимания из архитектуры Transformer.
|
||||
Выполняет scaled dot-product attention с маскированием будущих позиций (causal attention).
|
||||
|
||||
Основной алгоритм:
|
||||
1. Линейные преобразования входных данных в Q (query), K (key), V (value)
|
||||
2. Вычисление scores = Q·K^T / sqrt(d_k)
|
||||
3. Применение causal маски (заполнение -inf будущих позиций)
|
||||
4. Softmax для получения весов внимания
|
||||
5. Умножение весов на значения V
|
||||
|
||||
Пример использования:
|
||||
>>> attention = HeadAttention(emb_size=64, head_size=32, max_seq_len=128)
|
||||
>>> x = torch.randn(1, 10, 64) # [batch_size, seq_len, emb_size]
|
||||
>>> output = attention(x) # [1, 10, 32]
|
||||
|
||||
Параметры:
|
||||
emb_size (int): Размер входного эмбеддинга
|
||||
head_size (int): Размерность выхода головы внимания
|
||||
max_seq_len (int): Максимальная длина последовательности
|
||||
|
||||
Примечания:
|
||||
- Использует нижнетреугольную маску для предотвращения "заглядывания в будущее"
|
||||
- Автоматически адаптируется к разным версиям PyTorch
|
||||
- Поддерживает batch-обработку входных данных
|
||||
"""
|
||||
def __init__(self, emb_size: int, head_size: int, max_seq_len: int):
|
||||
super().__init__()
|
||||
self._emb_size = emb_size
|
||||
self._head_size = head_size
|
||||
self._max_seq_len = max_seq_len
|
||||
|
||||
# Линейные преобразования для Q, K, V
|
||||
self._k = nn.Linear(emb_size, head_size)
|
||||
self._q = nn.Linear(emb_size, head_size)
|
||||
self._v = nn.Linear(emb_size, head_size)
|
||||
|
||||
# Создание causal маски
|
||||
mask = torch.tril(torch.ones(max_seq_len, max_seq_len))
|
||||
self.register_buffer('_tril_mask', mask.bool() if hasattr(torch, 'bool') else mask.byte())
|
||||
|
||||
def forward(self, x: torch.Tensor, use_cache: bool = True, cache: tuple = None) -> tuple:
|
||||
"""
|
||||
Прямой проход через слой внимания.
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор формы [batch_size, seq_len, emb_size]
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Выходной тензор формы [batch_size, seq_len, head_size]
|
||||
|
||||
Исключения:
|
||||
ValueError: Если длина последовательности превышает max_seq_len
|
||||
|
||||
Пример внутренних преобразований:
|
||||
Для входа x.shape = [2, 5, 64]:
|
||||
1. Q/K/V преобразования -> [2, 5, 32]
|
||||
2. Scores = Q·K^T -> [2, 5, 5]
|
||||
3. После маски и softmax -> [2, 5, 5]
|
||||
4. Умножение на V -> [2, 5, 32]
|
||||
"""
|
||||
seq_len = x.shape[1]
|
||||
if seq_len > self._max_seq_len:
|
||||
raise ValueError(f"Длина последовательности {seq_len} превышает максимум {self._max_seq_len}")
|
||||
|
||||
k = self._k(x) # [B, T, hs]
|
||||
q = self._q(x) # [B, T, hs]
|
||||
v = self._v(x) # [B, T, hs]
|
||||
|
||||
if cache is not None:
|
||||
k_cache, v_cache = cache
|
||||
k = torch.cat([k_cache, k], dim=1) # [B, cache_len + T, hs]
|
||||
v = torch.cat([v_cache, v], dim=1) # [B, cache_len + T, hs]
|
||||
|
||||
scores = q @ k.transpose(-2, -1) / sqrt(self._head_size)
|
||||
|
||||
if cache is None:
|
||||
scores = scores.masked_fill(~self._tril_mask[:seq_len, :seq_len], float('-inf'))
|
||||
|
||||
weights = F.softmax(scores, dim=-1)
|
||||
x_out = weights @ v # [B, T, hs]
|
||||
|
||||
if use_cache is True:
|
||||
return (x_out, (k, v))
|
||||
else:
|
||||
return (x_out, None)
|
||||
134
llm/src/llm/core/mistral_decoder.py
Normal file
134
llm/src/llm/core/mistral_decoder.py
Normal file
@@ -0,0 +1,134 @@
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from llm.core.rms_norm import RMSNorm
|
||||
from llm.core.swi_glu import SwiGLU
|
||||
from llm.core.rope import RoPE
|
||||
from llm.core.group_query_attention import GroupedQueryAttention
|
||||
|
||||
class MistralDecoder(nn.Module):
|
||||
"""
|
||||
MistralDecoder — стек декодирующих блоков, реализующий архитектуру Mistral-style Transformer.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
Этот класс описывает один или несколько блоков декодера, включающих Grouped Query Attention (GQA),
|
||||
sliding window attention и SwiGLU feed-forward, как реализовано в моделях Mistral и Llama 2.
|
||||
|
||||
Ключевые особенности архитектуры:
|
||||
---------------------------------
|
||||
- Использует GQA: для каждого токена вычисляется attention c раздельным числом Q и KV голов (сильно ускоряет LLM).
|
||||
- Sliding Window Attention: внимание ограничено окном из window_size элементов (ускоряет обработку длинных текстов).
|
||||
- Rotary Positional Embedding (RoPE): позиционная информация интегрируется вращением Q/K.
|
||||
- RMSNorm перед и после внимания и FFN (устойчивое обучение).
|
||||
- SwiGLU в качестве нелинейности вместо стандартного GELU (больше capacity в модели).
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
num_layers : int — сколько блоков-декодеров в стеке
|
||||
параметры GQA: num_q_heads, num_kv_heads, emb_size, head_size, max_seq_len, window_size, rope, dropout
|
||||
- все они идут в каждый слой (блок) декодера
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> decoder = MistralDecoder(
|
||||
... num_q_heads=8, num_kv_heads=2, emb_size=256, head_size=32,
|
||||
... max_seq_len=4096, window_size=256, rope=rope, dropout=0.1)
|
||||
>>> x = torch.randn(2, 512, 256)
|
||||
>>> out, cache = decoder(x)
|
||||
>>> print(out.shape) # torch.Size([2, 512, 256])
|
||||
|
||||
Подробнее:
|
||||
----------
|
||||
- Mistral: https://arxiv.org/abs/2310.06825
|
||||
- Llama 2: https://arxiv.org/abs/2307.09288
|
||||
- Open LLM обзор: https://huggingface.co/blog/mistral
|
||||
|
||||
"""
|
||||
def __init__(self,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
emb_size: int,
|
||||
head_size: int,
|
||||
max_seq_len: int,
|
||||
window_size: int,
|
||||
rope: RoPE,
|
||||
dropout: float = 0.1
|
||||
):
|
||||
"""
|
||||
Инициализация стека декодеров MistralDecoder.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
num_layers : int
|
||||
Сколько слоёв (декодеров/GQA-блоков) собрать в стек.
|
||||
num_q_heads : int
|
||||
Количество Query-heads в attention (их больше, экономит память).
|
||||
num_kv_heads : int
|
||||
Количество Key/Value-heads в attention (их меньше для быстрой генерации).
|
||||
emb_size : int
|
||||
Размерность embedding (должна делиться на num_q_heads без остатка).
|
||||
head_size : int
|
||||
Размер одного attention head.
|
||||
max_seq_len : int
|
||||
Максимально обрабатываемая длина последовательности.
|
||||
window_size : int
|
||||
Размер окна для sliding window attention.
|
||||
rope : RoPE
|
||||
Rotary Positional Embedding для Q/K.
|
||||
dropout : float, опционально
|
||||
Dropout на каждом attention/FFN (по умолчанию 0.1).
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Собираются num_layers Sequential-блоков из GQA + SwiGLU + RMSNorm.
|
||||
- Все параметры передаются в каждый слой (блок).
|
||||
"""
|
||||
super().__init__()
|
||||
self._heads = GroupedQueryAttention(
|
||||
num_q_heads=num_q_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
emb_size=emb_size,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
window_size=window_size,
|
||||
rope=rope,
|
||||
dropout=dropout
|
||||
)
|
||||
self._ff = SwiGLU(emb_size=emb_size, dropout=dropout)
|
||||
self._norm1 = RMSNorm(emb_size)
|
||||
self._norm2 = RMSNorm(emb_size)
|
||||
|
||||
def forward(self, x: torch.Tensor, mask: torch.Tensor = None, use_cache: bool = True, cache: list = None) -> torch.Tensor:
|
||||
"""
|
||||
Прямой проход через стек MistralDecoder.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Входные эмбеддинги (обычно shape [batch, seq_len, emb_size]).
|
||||
use_cache : bool, по умолчанию True
|
||||
Включить ли кэширование для ускорения генерации (авторегрессия).
|
||||
cache : list, опционально
|
||||
Предыдущий кеш attention-блоков (или None).
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
out : torch.Tensor
|
||||
Тензор после декодирования (shape соответствует x).
|
||||
new_cache : list (или None)
|
||||
Новый кэш attention для дальнейшей генерации (или None, если use_cache=False).
|
||||
|
||||
"""
|
||||
norm1_out = self._norm1(x)
|
||||
attention, kv_caches = self._heads(norm1_out, mask, use_cache=use_cache, cache=cache)
|
||||
out = attention + x
|
||||
|
||||
norm2_out = self._norm2(out)
|
||||
ffn_out = self._ff(norm2_out)
|
||||
|
||||
if use_cache is True:
|
||||
return (ffn_out + out, kv_caches)
|
||||
else:
|
||||
return (ffn_out + out, None)
|
||||
211
llm/src/llm/core/mixtral_decoder.py
Normal file
211
llm/src/llm/core/mixtral_decoder.py
Normal file
@@ -0,0 +1,211 @@
|
||||
|
||||
|
||||
from torch import nn
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from llm.core.rope import RoPE
|
||||
from llm.core.group_query_attention import GroupedQueryAttention
|
||||
from llm.core.moe import MoE
|
||||
from llm.core.rms_norm import RMSNorm
|
||||
|
||||
class MixtralDecoder(nn.Module):
|
||||
"""
|
||||
MixtralDecoder — декодерный блок для Mixtral/MoE-трансформеров (см. Mixtral 8x7B, Mistral v0.2 и др.).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
MixtralDecoder реализует один модульный слой глубокой трансформерной архитектуры с Mixture-of-Experts (MoE) Feed-Forward Network и Grouped Query Attention (GQA).
|
||||
Поддерживает разреженную активацию и масштабируемое количество экспертов, оптимально для больших LLM.
|
||||
|
||||
Архитектура блока:
|
||||
------------------
|
||||
- RMSNorm -> Grouped Query Attention (GQA)
|
||||
- skip-connection
|
||||
- RMSNorm -> MoE (SwiGLU-эксперты)
|
||||
- skip-connection
|
||||
|
||||
Для входа `x` проходит:
|
||||
1. norm1_out = RMSNorm(x)
|
||||
2. attention, kv_caches = GQA(norm1_out, ...)
|
||||
3. out = attention + x # residual connection
|
||||
4. norm2_out = RMSNorm(out)
|
||||
5. ffn_out = MoE(norm2_out)
|
||||
6. return (ffn_out + out, kv_caches)
|
||||
|
||||
Теоретическая мотивация:
|
||||
------------------------
|
||||
- Использование MoE (см. https://arxiv.org/abs/1701.06538) позволяет кратно увеличивать capacity без роста затрат на ff-часть.
|
||||
- Grouped Query Attention эффективно масштабирует self-attention для больших моделей (см. Mistral, Llama 2/3).
|
||||
- RMSNorm (Root Mean Square LayerNorm) стабилизирует градиенты и память.
|
||||
- Является строительным блоком для стека декодеров в Mixtral-моделях (см. Mixtral, Mistral, LLaMA).
|
||||
|
||||
Аргументы конструктора:
|
||||
----------------------
|
||||
num_q_heads : int
|
||||
Число query-голов в attention.
|
||||
num_kv_heads : int
|
||||
Число key-value голов (группировка ключей/values).
|
||||
emb_size : int
|
||||
Скрытый размер эмбеддинга.
|
||||
head_size : int
|
||||
Размерность одной головы (emb_size // num_q_heads).
|
||||
max_seq_len : int
|
||||
Максимальная поддерживаемая длина последовательности.
|
||||
num_experts : int
|
||||
Количество «экспертов» (MoE).
|
||||
top_k_experts : int
|
||||
Сколько одновременно экспертов активируется для одного токена.
|
||||
window_size : int
|
||||
Размер окна внимания (используется для efficient attention).
|
||||
rope : RoPE
|
||||
Реализация позиционного кодирования RoPE.
|
||||
dropout : float
|
||||
Вероятность Dropout для регуляризации.
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> decoder = MixtralDecoder(... параметры ...)
|
||||
>>> x = torch.randn(batch, seq, emb_size)
|
||||
>>> out, cache = decoder(x, mask=None, use_cache=True)
|
||||
>>> out.shape
|
||||
|
||||
Литература и ссылки:
|
||||
--------------------
|
||||
- Mixtral 8x7B: https://mistral.ai/news/mixtral-of-experts/
|
||||
- Shazeer et al., “Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer”, 2017. https://arxiv.org/abs/1701.06538
|
||||
- Mistral paper: https://arxiv.org/abs/2310.06825
|
||||
- GQA: https://arxiv.org/abs/2305.14236
|
||||
- RMSNorm: https://arxiv.org/abs/1910.07467
|
||||
|
||||
"""
|
||||
def __init__(self,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
emb_size: int,
|
||||
head_size: int,
|
||||
max_seq_len: int,
|
||||
num_experts: int,
|
||||
top_k_experts: int,
|
||||
window_size: int,
|
||||
rope: RoPE,
|
||||
dropout: float = 0.1
|
||||
):
|
||||
"""
|
||||
Конструктор декодерного блока MixtralDecoder.
|
||||
|
||||
Осуществляет инициализацию всех под-компонентов слоя: Attention (Grouped Query Attention), MoE (Mixture-of-Experts, SwiGLU)
|
||||
и нормализации (RMSNorm). Позволяет гибко настраивать архитектуру под специфику задач и размеры LLM.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
num_q_heads : int
|
||||
Количество голов внимания (queries) в механизме GroupedQueryAttention.
|
||||
Чем больше — тем тоньше дискретизация внимания по подпространствам признаков.
|
||||
num_kv_heads : int
|
||||
Количество групп ключей/значений (key-value heads) для GQA.
|
||||
Позволяет балансировать производительность и память.
|
||||
emb_size : int
|
||||
Размерность эмбеддингового пространства внутри слоя (hidden).
|
||||
head_size : int
|
||||
Размерность одной attention-головы. Обычно emb_size // num_q_heads.
|
||||
max_seq_len : int
|
||||
Максимально поддерживаемая длина токенизированной последовательности.
|
||||
num_experts : int
|
||||
Количество экспертов в слое MoE (размер пула SwiGLU-экспертов).
|
||||
top_k_experts : int
|
||||
Сколько экспертов по роутингу активируется на 1 токен (разреженность — эффективная экономия вычислений).
|
||||
window_size : int
|
||||
Размер окна для attention (может использоваться для ограничения receptive field, как в Mistral).
|
||||
rope : RoPE
|
||||
Объект позиционного кодирования RoPE (Rotary Positional Embedding), необходим для архитектуры внимания.
|
||||
dropout : float, по умолчанию 0.1
|
||||
Вероятность зануляции выходных значений для регуляризации и борьбы с переобучением.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> decoder = MixtralDecoder(
|
||||
... num_q_heads=8,
|
||||
... num_kv_heads=2,
|
||||
... emb_size=256,
|
||||
... head_size=32,
|
||||
... max_seq_len=1024,
|
||||
... num_experts=4,
|
||||
... top_k_experts=2,
|
||||
... window_size=128,
|
||||
... rope=rope_module,
|
||||
... dropout=0.05
|
||||
... )
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
self._heads = GroupedQueryAttention(
|
||||
num_q_heads=num_q_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
emb_size=emb_size,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
window_size=window_size,
|
||||
rope=rope,
|
||||
dropout=dropout
|
||||
)
|
||||
self._ff = MoE(
|
||||
emb_size=emb_size,
|
||||
num_experts=num_experts,
|
||||
top_k_experts=top_k_experts,
|
||||
dropout=dropout
|
||||
)
|
||||
self._norm1 = RMSNorm(emb_size)
|
||||
self._norm2 = RMSNorm(emb_size)
|
||||
|
||||
def forward(self, x: torch.Tensor, mask: torch.Tensor = None, use_cache: bool = True, cache: list = None) -> torch.Tensor:
|
||||
"""
|
||||
Прямой проход (forward) через декодерный блок MixtralDecoder.
|
||||
|
||||
Данный метод реализует последовательную обработку входных скрытых состояний (x) через:
|
||||
- нормализацию (RMSNorm),
|
||||
- attention-модуль (Grouped Query Attention) с опциональным применением маски и кэша ключей/значений для ускорения инференса,
|
||||
- остаточное сложение (residual connection),
|
||||
- повторную нормализацию,
|
||||
- feed-forward блок на основе Mixture-of-Experts (MoE),
|
||||
- финальное остаточное сложение.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Входной скрытый тензор формы [batch_size, seq_len, emb_size] — результат эмбеддинга токенов либо предыдущего слоя.
|
||||
mask : torch.Tensor, optional
|
||||
(Необязательно) Маска внимания для ограничения области self-attention (например, для автоперемешивания или causal-LLM-моделей).
|
||||
use_cache : bool, по умолчанию True
|
||||
Если True — сохраняет кэш ключей/значений attention для ускорения авторегрессии (инференса).
|
||||
cache : list, optional
|
||||
(Необязательно) Предварительно вычисленный кеш attention (для ускорения генерации длинного текста).
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
Tuple[torch.Tensor, Any]:
|
||||
- Первый элемент: скрытый тензор выхода слоя с той же формой, что вход (последовательный residual из attention и MoE-блока).
|
||||
- Второй элемент: обновлённый кэш attention (если use_cache=True), иначе None.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> out, cache = decoder(x, mask=att_mask, use_cache=True, cache=old_cache)
|
||||
>>> out.shape # [batch_size, seq_len, emb_size]
|
||||
|
||||
Примечания:
|
||||
-----------
|
||||
- Для autoregressive-генерации (GPT-like режимов) следует передавать mask и использовать use_cache=True.
|
||||
- Реализация поддерживает произвольные батчи и длины последовательностей, в пределах max_seq_len слоя.
|
||||
- Модуль MixtralDecoder обычно используется в виде стека (несколько подряд) внутри крупной LLM.
|
||||
|
||||
"""
|
||||
norm1_out = self._norm1(x)
|
||||
attention, kv_caches = self._heads(norm1_out, mask, use_cache=use_cache, cache=cache)
|
||||
out = attention + x
|
||||
|
||||
norm2_out = self._norm2(out)
|
||||
ffn_out = self._ff(norm2_out)
|
||||
|
||||
if use_cache is True:
|
||||
return (ffn_out + out, kv_caches)
|
||||
else:
|
||||
return (ffn_out + out, None)
|
||||
229
llm/src/llm/core/moe.py
Normal file
229
llm/src/llm/core/moe.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
from llm.core.swi_glu import SwiGLU
|
||||
|
||||
class MoE(nn.Module):
|
||||
"""
|
||||
MoE (Mixture of Experts) — слой «смеси экспертов» для современных трансформерных архитектур с разреженной активацией.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
Класс реализует слой разреженного условного вычисления для увеличения capacity трансформеров без роста вычислительных затрат.
|
||||
Для каждого токена из последовательности выбирается (с помощью роутера) наиболее подходящее подмножество экспертов (малых нейросетей).
|
||||
Итоговый выход формируется как взвешенная сумма откликов экспертов, выбранных для данного токена.
|
||||
|
||||
Архитектурная схема:
|
||||
---------------------
|
||||
- Для каждого входного токена `x` роутер (обычно один Linear-слой) предсказывает skor, насколько каждый из `num_experts` релевантен.
|
||||
- Для каждого токена выбираются top_k_experts с максимальными skor; только они обрабатывают этот токен.
|
||||
- Каждый эксперт здесь представлен отдельным экземпляром блока `SwiGLU` (может быть любая небольшая feed-forward сеть).
|
||||
- Выход каждого эксперта умножается на индивидуальный вес (softmax по skor) — агрегируется взвешенная сумма.
|
||||
- Dropout применяется к итоговому выходу.
|
||||
|
||||
Математика (коротко):
|
||||
---------------------
|
||||
Пусть X ∈ R^{BxSxD} — вход,
|
||||
E — число экспертов,
|
||||
K — число активируемых экспертов на токен.
|
||||
r(x) = softmax(W_r x) — роутинг-логиты, top-K берём индексы и веса.
|
||||
Для каждого токена:
|
||||
y_j = Expert_j(x)
|
||||
y = sum_j(w_j * y_j), где j пробегает по выбранным экспертам
|
||||
Output: Y ∈ R^{BxSxD}
|
||||
|
||||
Аргументы конструктора:
|
||||
----------------------
|
||||
emb_size : int
|
||||
Размерность входных/выходных векторов (обычно совпадает с embedding модели).
|
||||
num_experts : int
|
||||
Общее число экспертов внутри слоя MoE.
|
||||
top_k_experts : int
|
||||
Сколько экспертов активировать и агрегировать на каждом токене (обычно 2-8).
|
||||
dropout : float, по умолчанию 0.1
|
||||
Dropout к выходу агрегатора.
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> moe = MoE(emb_size=512, num_experts=8, top_k_experts=2, dropout=0.1)
|
||||
>>> x = torch.randn(4, 16, 512)
|
||||
>>> y = moe(x)
|
||||
>>> y.shape # torch.Size([4, 16, 512])
|
||||
|
||||
Литература:
|
||||
-----------
|
||||
- Shazeer, N. et al. “Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer”, 2017. https://arxiv.org/abs/1701.06538
|
||||
- Fedus, W., Zoph, B., & Shazeer, N. “Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity”, 2021. https://arxiv.org/abs/2101.03961
|
||||
- Mistral/Mixtral: https://mistral.ai/news/mixtral-of-experts/
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
emb_size: int,
|
||||
num_experts: int,
|
||||
top_k_experts: int,
|
||||
dropout: float = 0.1,
|
||||
):
|
||||
"""
|
||||
Конструктор слоя MoE (Mixture of Experts).
|
||||
|
||||
Позволяет создать слой, состоящий из набора экспертов (например, отдельных небольших feedforward-нейросетей) и роутера,
|
||||
который будет для каждого токена определять наиболее релевантных экспертов.
|
||||
Часть экспертов (top_k_experts) активируется для каждого токена, остальные — пропускаются.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
emb_size : int
|
||||
Размерность входных и выходных векторов (embedding size).
|
||||
Определяет, над каким пространством признаков будет работать роутер и эксперты.
|
||||
Например, если скрытый размер слоя трансформера 512, сюда нужно передать 512.
|
||||
|
||||
num_experts : int
|
||||
Общее количество экспертов в слое MoE.
|
||||
Чем больше экспертов — тем больше capacity у модели, но тем выше требования к RAM/VRAM при обучении.
|
||||
Пример: 8, 16, 32, 64.
|
||||
|
||||
top_k_experts : int
|
||||
Сколько экспертов одновременно будет обрабатывать каждый токен.
|
||||
Обычно 2–8. Меньшее значение — выше разреженность, больше экономия вычислений.
|
||||
|
||||
dropout : float, по умолчанию 0.1
|
||||
Вероятность зануления значений на выходе после агрегации откликов экспертов.
|
||||
Используется для регуляризации (борьбы с переобучением).
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> moe = MoE(emb_size=256, num_experts=8, top_k_experts=2, dropout=0.1)
|
||||
>>> print(moe)
|
||||
MoE( ... )
|
||||
|
||||
Теория:
|
||||
-------
|
||||
Слой строит:
|
||||
- Линейный роутер (Linear(emb_size, num_experts)): выдает «важность» каждого эксперта для токена.
|
||||
- Список из num_experts экспертов (в данной реализации — SwiGLU-блоки).
|
||||
|
||||
При каждом проходе для каждого токена выбираются top_k_experts наиболее релевантных экспертов,
|
||||
их ответы агрегируются взвешенной суммой (softmax по роутерным логитам).
|
||||
"""
|
||||
super().__init__()
|
||||
if top_k_experts > num_experts:
|
||||
raise ValueError(f"top_k_experts ({top_k_experts}) должен быть меньше или равен num_experts ({num_experts})!")
|
||||
self._num_experts = num_experts
|
||||
self._top_k_experts = top_k_experts
|
||||
|
||||
self._router = nn.Linear(emb_size, num_experts)
|
||||
self._experts = nn.ModuleList([SwiGLU(
|
||||
emb_size=emb_size,
|
||||
dropout=dropout,
|
||||
) for _ in range(num_experts)])
|
||||
self._dropout = nn.Dropout(dropout)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
Прямой проход (forward) через слой MoE.
|
||||
|
||||
Для входной последовательности скрытых состояний (обычно из предыдущего слоя трансформера)
|
||||
данный метод динамически выбирает для каждого токена топ-k наиболее релевантных экспертов с помощью роутера,
|
||||
пропускает соответствующие токены через выбранных экспертов и агрегирует их результаты.
|
||||
|
||||
Математически:
|
||||
--------------
|
||||
1. Для каждого токена вычисляются логиты маршрутизатора (роутера):
|
||||
router_logits = Linear(x) ∈ ℝ^{batch, seq, num_experts}
|
||||
2. Выбираются top_k экспертов (topk_indices) и соответствующие им softmax-веса (topk_weights).
|
||||
3. Каждый эксперт обрабатывает только свой поднабор токенов.
|
||||
4. Результат агрегируется — отклик эксперта умножается на вес, ответы суммируются для каждого токена.
|
||||
5. На результат применяется dropout для регуляризации.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Трёхмерный входной тензор формы [batch_size, seq_length, emb_size],
|
||||
где batch_size — размер батча, seq_length — длина последовательности, emb_size — размерность эмбеддинга.
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
torch.Tensor :
|
||||
Тензор той же формы [batch_size, seq_length, emb_size] — результат комбинирования выходов выбранных экспертов
|
||||
с учетом softmax-весов маршрутизатора и dropout'а.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> y = moe(x)
|
||||
>>> print(y.shape)
|
||||
torch.Size([batch_size, seq_length, emb_size])
|
||||
|
||||
Примечание:
|
||||
-----------
|
||||
- Каждый токен чаще всего активирует только подмножество экспертов.
|
||||
- Остальные эксперты вычислительно “спят”, что позволяет строить очень большие (по параметрам) модели с малым ростом затрат.
|
||||
- Работа с распределением топ-к экспертов и агрегирование с весами реализовано автоматически.
|
||||
|
||||
"""
|
||||
batch_size, seq_len, emb_size = x.shape
|
||||
|
||||
# 1. Пропускаем через роутер
|
||||
router_logits = self._router(x) # [batch_size, seq_len, num_experts]
|
||||
|
||||
# 2. Отбираем топ-k экспертов для каждого токена
|
||||
topk_logits, topk_indices = torch.topk(
|
||||
router_logits,
|
||||
k=self._top_k_experts,
|
||||
dim=-1
|
||||
) # topk_logits: [batch_size, seq_len, top_k]
|
||||
# topk_indices: [batch_size, seq_len, top_k]
|
||||
|
||||
# 3. Получаем веса через softmax и нормируем
|
||||
topk_weights = F.softmax(topk_logits, dim=-1) # [batch_size, seq_len, top_k]
|
||||
|
||||
# 4. Создаём нулевой тензор для результата
|
||||
output = torch.zeros_like(x) # [batch_size, seq_len, emb_size]
|
||||
|
||||
# 5. Проходим по всем экспертам
|
||||
for expert_id in range(self._num_experts):
|
||||
# Шаг 1: Создаём маску - где находится текущий эксперт в топ-k
|
||||
expert_mask = (topk_indices == expert_id) # [batch_size, seq_len, top_k]
|
||||
# Шаг 2: Проверяем, выбран ли эксперт хотя бы одним токеном
|
||||
if not expert_mask.any():
|
||||
continue # Эксперт никем не выбран, переходим к следующему
|
||||
|
||||
# Шаг 3: Находим токены, которые выбрали этого эксперта
|
||||
# (хотя бы в одной из top_k позиций)
|
||||
token_mask = expert_mask.any(dim=-1) # [batch_size, seq_len]
|
||||
|
||||
# Шаг 4: Отбираем токены из x
|
||||
# Отбираем токены для этого эксперта
|
||||
expert_input = x[token_mask]
|
||||
|
||||
# Пропускаем через эксперта
|
||||
# Добавляем batch dimension для SwiGLU и затем убираем
|
||||
expert_output = self._experts[expert_id](
|
||||
expert_input.unsqueeze(0)
|
||||
).squeeze(0)
|
||||
|
||||
# Получаем веса для этого эксперта
|
||||
# Для каждого токена может быть несколько весов (если эксперт в топ-k несколько раз)
|
||||
# Но на практике каждый эксперт появляется максимум 1 раз в топ-k
|
||||
# Находим веса: где expert_mask == True, берём соответствующий вес
|
||||
weights_for_expert = torch.zeros(
|
||||
batch_size, seq_len, device=x.device
|
||||
)
|
||||
|
||||
# Для каждой позиции в топ-k
|
||||
for k in range(self._top_k_experts):
|
||||
mask_k = topk_indices[:, :, k] == expert_id
|
||||
weights_for_expert[mask_k] = topk_weights[:, :, k][mask_k]
|
||||
|
||||
# Отбираем только веса для выбранных токенов
|
||||
selected_weights = weights_for_expert[token_mask] # [num_selected_tokens]
|
||||
|
||||
|
||||
# Перемножьте выход эксперта на веса текущего эксперта.
|
||||
weighted_output = selected_weights.unsqueeze(-1) * expert_output
|
||||
|
||||
# Помещаем результат на своё место в выходном тензоре
|
||||
output[token_mask] += weighted_output
|
||||
|
||||
out = self._dropout(output)
|
||||
|
||||
return out
|
||||
@@ -1,115 +1,263 @@
|
||||
from torch import nn
|
||||
import torch
|
||||
from .head_attention import HeadAttention
|
||||
import torch.nn.functional as F
|
||||
from .rope import RoPE
|
||||
|
||||
|
||||
class MultiHeadAttention(nn.Module):
|
||||
"""
|
||||
Реализация механизма многоголового внимания (Multi-Head Attention) из архитектуры Transformer.
|
||||
Multi-Head Attention (Многоголовое внимание)
|
||||
============================================
|
||||
|
||||
Основные характеристики:
|
||||
- Параллельная обработка входных данных несколькими головами внимания
|
||||
- Поддержка маскирования (causal mask и пользовательские маски)
|
||||
- Финальная проекция с dropout регуляризацией
|
||||
Что такое Multi-Head Attention?
|
||||
-------------------------------
|
||||
Это ключевой компонент трансформеров, который позволяет "смотреть" на разные части предложения
|
||||
одновременно с нескольких независимых ракурсов ("голов"). Всё, что делает Single-Head Attention — только гораздо мощнее и глубже!
|
||||
|
||||
Математическое описание:
|
||||
MultiHead(Q, K, V) = Concat(head_1, ..., head_h)W^O
|
||||
где head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)
|
||||
Зачем это нужно?
|
||||
----------------
|
||||
- Модель может учиться одновременно учитывать и локальные, и глобальные взаимосвязи между токенами.
|
||||
- Каждая attention head "ловит" свой собственный смысл/зависимости, и на выходе они объединяются.
|
||||
- Это значительно улучшает понимание сложных зависимостей в тексте, особенно на длинных последовательностях.
|
||||
|
||||
Примеры использования:
|
||||
Как работает алгоритм? (основная схема)
|
||||
---------------------------------------
|
||||
1. Генерируются Q, K, V (query, key, value) — по отдельной проекции для каждой головы.
|
||||
2. Для каждой головы: attention(Q, K, V) = softmax(Q·K^T / sqrt(d)) · V
|
||||
3. Все головы "склеиваются" (concatenate) и прогоняются через общий финальный линейный слой.
|
||||
|
||||
1. Базовый пример:
|
||||
>>> mha = MultiHeadAttention(num_heads=8, emb_size=512, head_size=64, max_seq_len=1024)
|
||||
>>> x = torch.randn(2, 50, 512) # [batch_size, seq_len, emb_size]
|
||||
>>> output = mha(x) # [2, 50, 512]
|
||||
Почему это работает?
|
||||
--------------------
|
||||
- Даёт трансформеру многомерное восприятие текста.
|
||||
- Позволяет эффективно обучаться на задачах, где порядок и "дальние" связи важнее, чем простое соседство.
|
||||
|
||||
2. С использованием маски:
|
||||
>>> mask = torch.tril(torch.ones(50, 50)) # Causal mask
|
||||
>>> output = mha(x, mask)
|
||||
Что принимается на вход:
|
||||
------------------------
|
||||
- x: shape [batch, seq_len, embed_dim] — обычный batched-embed тензор.
|
||||
- mask (опционально): shape [seq_len, seq_len] — маска для автогерерации или causal attention.
|
||||
|
||||
3. Интеграция в Transformer:
|
||||
>>> # В составе Transformer слоя
|
||||
>>> self.attention = MultiHeadAttention(...)
|
||||
>>> x = self.attention(x, mask)
|
||||
Какие параметры важны:
|
||||
----------------------
|
||||
- num_heads: сколько attention heads внутри (обычно 4, 8, 16...).
|
||||
- embed_dim: исходная размерность входного тензора.
|
||||
- head_size: размер одной attention-head (обычно embed_dim // num_heads).
|
||||
- max_seq_len: максимальная длина последовательности для маски.
|
||||
|
||||
Что возвращает:
|
||||
---------------
|
||||
- output: shape [batch, seq_len, embed_dim] — результат применения всех attention heads.
|
||||
- (опционально) cache: кэш для Q/K/V (нужно для генерации по одному токену).
|
||||
|
||||
Особенности реализации:
|
||||
-----------------------
|
||||
- Оптимизированно работает через матричные умножения (без python for циклов!).
|
||||
- Включена поддержка causal attention (маска, предотвращающая «заглядывание в будущее»).
|
||||
- Является ядром любого трансформера (и LLM!).
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> attn = MultiHeadAttention(num_heads=8, embed_dim=256, head_size=32, max_seq_len=1024)
|
||||
>>> x = torch.randn(2, 128, 256) # [batch, seq_len, embed_dim]
|
||||
>>> context, _ = attn(x)
|
||||
>>> print(context.shape) # torch.Size([2, 128, 256])
|
||||
|
||||
Где прочитать подробнее:
|
||||
-------------------------
|
||||
- Attention is All You Need (Vaswani et al, 2017): https://arxiv.org/abs/1706.03762
|
||||
- Illustrated Transformer (blog): https://jalammar.github.io/illustrated-transformer/
|
||||
"""
|
||||
def __init__(self, num_heads: int, emb_size: int, head_size: int, max_seq_len: int, dropout: float = 0.1):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
emb_size: int,
|
||||
head_size: int,
|
||||
max_seq_len: int,
|
||||
rope: RoPE = None,
|
||||
dropout: float = 0.1,
|
||||
):
|
||||
"""
|
||||
Инициализация многоголового внимания.
|
||||
Конструктор многоголового внимания (MultiHeadAttention).
|
||||
|
||||
Параметры:
|
||||
num_heads (int): Количество голов внимания. Типичные значения: 4-16
|
||||
emb_size (int): Размерность входных и выходных эмбеддингов
|
||||
head_size (int): Размерность каждой головы внимания (обычно emb_size // num_heads)
|
||||
max_seq_len (int): Максимальная длина последовательности
|
||||
dropout (float): Вероятность dropout (по умолчанию 0.1)
|
||||
Здесь создаются все параметры и внутренние слои для эффективного параллельного внимания (attention) сразу из нескольких "голов".
|
||||
|
||||
Контрольные значения:
|
||||
- num_heads * head_size должно равняться emb_size
|
||||
- head_size обычно выбирают 32-128
|
||||
- max_seq_len зависит от задачи (512 для BERT, 2048 для GPT-3)
|
||||
Аргументы:
|
||||
----------
|
||||
num_heads : int
|
||||
Сколько attention-heads будет внутри слоя.
|
||||
Каждая “голова” учится видеть уникальные зависимости в тексте. Обычно это 4, 8, 16 и т.п.
|
||||
Чем больше голов — тем богаче контекст, но и больше памяти.
|
||||
emb_size : int
|
||||
Сколько float-значений в каждом входном векторе (размерность embedding).
|
||||
Обычно это 256, 512, 768, 1024 и т.д.
|
||||
head_size : int
|
||||
Сколько компонент будет у каждой головы внимания.
|
||||
Важно: num_heads * head_size должно ровно совпадать с emb_size!
|
||||
Обычно head_size = emb_size // num_heads.
|
||||
max_seq_len : int
|
||||
Максимально допустимая длина последовательности для attention/маски/генерации.
|
||||
Определяет размер буферов для causal mask.
|
||||
rope : RoPE, по умолчанию None
|
||||
Объект Rotary Positional Encoding (если хотите привнести продвинутое позиционное кодирование в attention).
|
||||
Не обязателен, но нужен для современных LLM (Llama, Mistral и пр.).
|
||||
dropout : float, по умолчанию 0.1
|
||||
Величина dropout (регуляризации) — помогает борьбе с переобучением. Чем больше, тем сильнее регуляризация.
|
||||
|
||||
Внутри конструктора происходит:
|
||||
-------------------------------
|
||||
- Создаются три линейных слоя для Q, K, V (“где смотреть” и “что вытаскивать” в attention).
|
||||
- Генерируется нижнетреугольная causal-маска (запрещает видеть будущее для автогерерации).
|
||||
- Создаётся финальный линейный слой для склейки всех голов в одно пространство emb_size.
|
||||
- Вводится dropout (случайное зануление, чтобы не было сильной зависимости внимания к отдельным "плейсам").
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> attn = MultiHeadAttention(num_heads=8, emb_size=256, head_size=32, max_seq_len=1024)
|
||||
"""
|
||||
super().__init__()
|
||||
self._heads = nn.ModuleList([
|
||||
HeadAttention(
|
||||
emb_size=emb_size,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len
|
||||
) for _ in range(num_heads)
|
||||
])
|
||||
self._num_heads = num_heads
|
||||
self._head_size = head_size
|
||||
self._max_seq_len = max_seq_len
|
||||
self._rope = rope
|
||||
|
||||
self._q = nn.Linear(emb_size, num_heads * head_size)
|
||||
self._k = nn.Linear(emb_size, num_heads * head_size)
|
||||
self._v = nn.Linear(emb_size, num_heads * head_size)
|
||||
|
||||
# Создание causal маски
|
||||
mask = torch.tril(torch.ones(max_seq_len, max_seq_len))
|
||||
self.register_buffer(
|
||||
"_tril_mask", mask.bool() if hasattr(torch, "bool") else mask.byte()
|
||||
)
|
||||
|
||||
self._layer = nn.Linear(head_size * num_heads, emb_size)
|
||||
self._dropout = nn.Dropout(dropout)
|
||||
|
||||
def forward(self, x: torch.Tensor, mask: torch.Tensor = None, use_cache: bool = True, cache: list = None):
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
mask: torch.Tensor = None,
|
||||
use_cache: bool = True,
|
||||
cache: list = None,
|
||||
):
|
||||
"""
|
||||
Прямой проход через слой многоголового внимания.
|
||||
Основной шаг \"многоголового внимания\": находит взаимосвязи между токенами
|
||||
в последовательности сразу из нескольких “ракурсов” (attention heads).
|
||||
|
||||
Подробное описание преобразований тензоров:
|
||||
1. Входной тензор [batch_size, seq_len, emb_size] разделяется на N голов:
|
||||
- Каждая голова получает тензор [batch_size, seq_len, head_size]
|
||||
2. Каждая голова вычисляет attention:
|
||||
- Вход: [batch_size, seq_len, head_size]
|
||||
- Выход: [batch_size, seq_len, head_size]
|
||||
3. Конкатенация результатов:
|
||||
- Объединенный выход: [batch_size, seq_len, num_heads * head_size]
|
||||
4. Линейная проекция:
|
||||
- Выход: [batch_size, seq_len, emb_size]
|
||||
5. Применение dropout
|
||||
Что делает этот метод:
|
||||
----------------------
|
||||
- Для каждого токена сравнивает его с остальными во входной последовательности.
|
||||
- Делает это одновременно через несколько attention heads (каждая head видит текст по-своему).
|
||||
- Итоговое “внимание” — это взвешенная сумма других токенов (контекста) для каждого токена.
|
||||
- Можно использовать кэш для генерации длинных последовательностей по одному токену (ускоряет инференс).
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор формы [batch_size, seq_len, emb_size]
|
||||
mask (torch.Tensor, optional): Маска внимания формы [seq_len, seq_len]
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Входной тензор формы [batch, seq_len, emb_size].
|
||||
Это ваши входные эмбеддинги (обычно после token + positional embedding).
|
||||
mask : torch.Tensor, опционально
|
||||
Матрица формы [seq_len, seq_len], задающая “разрешения” — кто может смотреть на кого (например, causal mask).
|
||||
Если не указана — используется внутренняя маска (например, для autoregressive генерации).
|
||||
use_cache : bool, по умолчанию True
|
||||
Нужно ли использовать кэш для KV attention (важно для ускорения генерации по одному токену).
|
||||
cache : list, опционально
|
||||
Предыдущий кэш Key/Value — для генерации текста по частям.
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Выходной тензор формы [batch_size, seq_len, emb_size]
|
||||
-----------
|
||||
- output: torch.Tensor формы [batch, seq_len, emb_size] — результат применения multi-head attention.
|
||||
- kv_caches: список новых KV для кэширования при генерации (или None).
|
||||
|
||||
Пример преобразований для emb_size=512, num_heads=8:
|
||||
Вход: [4, 100, 512]
|
||||
-> Каждая голова: [4, 100, 64]
|
||||
-> После внимания: 8 x [4, 100, 64]
|
||||
-> Конкатенация: [4, 100, 512]
|
||||
-> Проекция: [4, 100, 512]
|
||||
-> Dropout: [4, 100, 512]
|
||||
Важно:
|
||||
-------
|
||||
- Shape входа всегда [batch, seq_len, emb_size], выход тот же.
|
||||
- При seq_len > max_seq_len выбросит ошибку (безопасно для контроля переполнения буферов).
|
||||
- При использовании use_cache=True кешируется только последние токены (актуально для LLM).
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> attn = MultiHeadAttention(num_heads=8, emb_size=256, head_size=32, max_seq_len=1024)
|
||||
>>> x = torch.randn(2, 100, 256)
|
||||
>>> y, kv_cache = attn(x)
|
||||
>>> print(y.shape) # torch.Size([2, 100, 256])
|
||||
"""
|
||||
# 1. Вычисляем attention для каждой головы
|
||||
attention_results = []
|
||||
for i, head in enumerate(self._heads):
|
||||
head_cache = cache[i] if cache is not None else None
|
||||
result = head(x, use_cache=use_cache, cache=head_cache)
|
||||
attention_results.append(result)
|
||||
batch_size, seq_len, emb_size = x.shape
|
||||
|
||||
if seq_len > self._max_seq_len:
|
||||
raise ValueError(
|
||||
f"Длина последовательности {seq_len} превышает максимум {self._max_seq_len}"
|
||||
)
|
||||
|
||||
# Пропустите тензор x через матрицы Wq, Wk , Wv, чтобы получить матрицы запроса, ключа и значения.
|
||||
k = self._k(x) # [B, T, hs]
|
||||
q = self._q(x) # [B, T, hs]
|
||||
v = self._v(x) # [B, T, hs]
|
||||
|
||||
# Шаг 2: Изменение формы для multi-head
|
||||
# [batch_size, seq_len, num_heads * head_size]
|
||||
# -> [batch_size, seq_len, num_heads, head_size]
|
||||
q = q.reshape(batch_size, seq_len, self._num_heads, self._head_size)
|
||||
k = k.reshape(batch_size, seq_len, self._num_heads, self._head_size)
|
||||
v = v.reshape(batch_size, seq_len, self._num_heads, self._head_size)
|
||||
|
||||
outputs, caches = zip(*attention_results)
|
||||
attention_outputs = list(outputs)
|
||||
kv_caches = list(caches)
|
||||
|
||||
# 2. Объединяем результаты всех голов
|
||||
concatenated_attention = torch.cat(attention_outputs, dim=-1)
|
||||
|
||||
# 3. Transpose: [B, T, H, hs] -> [B, H, T, hs]
|
||||
q = q.transpose(1, 2)
|
||||
k = k.transpose(1, 2)
|
||||
v = v.transpose(1, 2)
|
||||
|
||||
start_pos = 0
|
||||
if cache is not None:
|
||||
k_cache, v_cache = cache
|
||||
cache_len = k_cache.shape[2]
|
||||
start_pos = cache_len
|
||||
|
||||
# Пропустите матрицы запроса и ключа через экземпляр rope, чтобы выполнить поворот.
|
||||
if self._rope is not None:
|
||||
# ✅ Применяем RoPE к Q и K (НЕ к V!)
|
||||
q = self._rope(q, start_pos=start_pos) # [B, T, hs]
|
||||
k = self._rope(k, start_pos=start_pos) # [B, T, hs]
|
||||
|
||||
# Если cache пришел, то объединяем кэш и одну строку из ключа и значения. Это будут новые key и value для последующих вычислений.
|
||||
# 5. Кэширование (для autoregressive generation)
|
||||
if cache is not None:
|
||||
k_cache, v_cache = cache
|
||||
k = torch.cat([k_cache, k], dim=2) # Concat по seq_len (dim=2)
|
||||
v = torch.cat([v_cache, v], dim=2)
|
||||
|
||||
# Перемножим матрицы запроса и ключа (транспонированную), чтобы вычислить матрицу внимания.
|
||||
# И разделить все значения в матрице внимания на корень из head_size.
|
||||
scores = q @ k.transpose(-2, -1) / (self._head_size ** 0.5)
|
||||
|
||||
# Если cache пришел, то маску не накладываем. Иначе наложите на матрицу внимания треугольную маску, созданную при инициализации. Все скрытые значения должны быть приведены к минус бесконечности: float('-inf').
|
||||
if cache is None:
|
||||
scores = scores.masked_fill(
|
||||
~self._tril_mask[:seq_len, :seq_len], float("-inf")
|
||||
)
|
||||
|
||||
# Применить к матрице внимания (построчно) функцию Softmax.
|
||||
weights = F.softmax(scores, dim=-1)
|
||||
|
||||
# Перемножим матрицу внимания и матрицу значения.
|
||||
x_out = weights @ v # [B, T, hs]
|
||||
|
||||
# Измените форму тензора на batch_size × seq_len × num_heads*head_size.
|
||||
# Transpose обратно и concatenate heads
|
||||
x_out = x_out.transpose(1, 2) # [B, T_q, H, hs]
|
||||
x_out = x_out.contiguous() # Важно для reshape!
|
||||
concatenated_attention = x_out.reshape(batch_size, seq_len, self._num_heads * self._head_size)
|
||||
|
||||
#concatenated_attention = x_out.reshape(batch_size, seq_len, self._num_heads * self._head_size)
|
||||
|
||||
# Пропустите получившийся тензор через последний линейный слой.
|
||||
# 3. Проецируем в пространство эмбеддингов
|
||||
projected_output = self._layer(concatenated_attention)
|
||||
|
||||
|
||||
# 4. Применяем dropout для регуляризации
|
||||
final_output = self._dropout(projected_output)
|
||||
|
||||
|
||||
if use_cache is True:
|
||||
return (final_output, kv_caches)
|
||||
return (final_output, (k, v))
|
||||
else:
|
||||
return (final_output, None)
|
||||
|
||||
@@ -1,66 +1,105 @@
|
||||
import torch
|
||||
from torch import nn, Tensor
|
||||
|
||||
|
||||
class PositionalEmbeddings(nn.Module):
|
||||
"""
|
||||
Класс для создания позиционных эмбеддингов через nn.Embedding.
|
||||
|
||||
Позиционные эмбеддинги используются в нейросетях для передачи информации
|
||||
о позиции элементов в последовательности (например, в Transformer).
|
||||
|
||||
Особенности:
|
||||
- Создаёт обучаемые позиционные эмбеддинги фиксированной длины
|
||||
- Поддерживает обработку последовательностей переменной длины
|
||||
- Автоматически размещает вычисления на том же устройстве, что и параметры
|
||||
|
||||
Args:
|
||||
max_seq_len (int): Максимальная длина последовательности
|
||||
emb_size (int): Размерность векторного представления позиций
|
||||
|
||||
Пример использования:
|
||||
>>> pos_encoder = PositionalEmbeddings(max_seq_len=100, emb_size=256)
|
||||
>>> # Получить эмбеддинги для последовательности из 10 элементов
|
||||
>>> embeddings = pos_encoder(10) # Tensor shape: [10, 256]
|
||||
>>> # Использование в модели
|
||||
>>> class MyModel(nn.Module):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.pos_emb = PositionalEmbeddings(100, 256)
|
||||
... def forward(self, x):
|
||||
... pos = self.pos_emb(x.size(1))
|
||||
... return x + pos # Добавляем позиционную информацию
|
||||
PositionalEmbeddings — классические позиционные эмбеддинги для трансформеров (absolute sinusoidal or learned).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Добавляет или конкатенирует форму позиционной информации к каждому входному токену (since Transformer cannot distinguish positions otherwise).
|
||||
- Используется во всех \"ранних\" трансформерах (GPT, BERT, T5), чаще всего в виде learnable или синусоидальных embeddings.
|
||||
|
||||
Архитектурные варианты:
|
||||
-----------------------
|
||||
- Learnable positional embeddings (как в GPT-2): обычный nn.Embedding инициализируется случайно, и веса учатся вместе с моделью.
|
||||
- Sinusoidal positional encoding (как в оригинальном Transformer): не имеет параметров, а создаётся по заданной формуле sin/cos(ω*x).
|
||||
|
||||
Принцип работы:
|
||||
---------------
|
||||
- Для каждой позиции t заполняется вектор emb_size длиной по формуле (или выбирается из weight matrix).
|
||||
- Эти вектора можно либо складывать с токеновыми эмбеддингами, либо конкатенировать.
|
||||
- Позволяет attention-механизму \"понимать\" порядок токенов/слов в последовательности.
|
||||
|
||||
Формулы (Or: Vaswani et al., 2017):
|
||||
------------------------------------
|
||||
PE(pos, 2i) = sin(pos / 10000^{2i/d})
|
||||
PE(pos, 2i+1) = cos(pos / 10000^{2i/d})
|
||||
где d = emb_size, pos = позиция (int), i = индекс пары компонент.
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
max_seq_len: int — максимально поддерживаемая длина последовательности
|
||||
emb_size: int — размер возвращаемого positional vector для каждой позиции
|
||||
(иногда выбирается вариант — learnable или фиксация через sin/cos)
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> pos = PositionalEmbeddings(max_seq_len=1024, emb_size=256)
|
||||
>>> p = pos(32) # Получить positional embeddings для 32 позиций
|
||||
>>> p.shape # torch.Size([32, 256])
|
||||
>>> token_emb = ... # [batch, seq_len, emb_size]
|
||||
>>> encoded = token_emb + p.unsqueeze(0) # Broadcast add
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Vaswani et al., \"Attention is All You Need\", 2017: https://arxiv.org/abs/1706.03762
|
||||
- GPT-2 implementation: https://github.com/openai/gpt-2
|
||||
- Почему positional encoding важен: https://kazemnejad.com/blog/transformer_architecture_positional_encoding/
|
||||
"""
|
||||
|
||||
def __init__(self, max_seq_len: int, emb_size: int):
|
||||
"""
|
||||
Инициализация позиционного энкодера.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
max_seq_len : int
|
||||
Максимальная длина последовательности (builds buffer for sin/cos or embedding)
|
||||
emb_size : int
|
||||
Длина позиционного вектора
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Если используется learned embedding: создаётся nn.Embedding (можно легко менять в будущем).
|
||||
- Если fixed (sin/cos): вычисляется и хранится буфер (max_seq_len, emb_size).
|
||||
"""
|
||||
super().__init__()
|
||||
self.max_seq_len = max_seq_len
|
||||
self.emb_size = emb_size
|
||||
self.embedding = nn.Embedding(
|
||||
num_embeddings=max_seq_len,
|
||||
embedding_dim=emb_size
|
||||
num_embeddings=max_seq_len, embedding_dim=emb_size
|
||||
)
|
||||
|
||||
def forward(self, seq_len: int, start_pos: int = 0) -> Tensor:
|
||||
"""
|
||||
Возвращает позиционные эмбеддинги для заданной длины последовательности.
|
||||
|
||||
Args:
|
||||
seq_len (int): Длина последовательности (1 <= seq_len <= max_seq_len)
|
||||
|
||||
Returns:
|
||||
Tensor: Тензор позиционных эмбеддингов формы [seq_len, emb_size]
|
||||
|
||||
Raises:
|
||||
IndexError: Если seq_len выходит за допустимые границы
|
||||
|
||||
Получить positional embeddings для последовательности длиной seq_len.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
seq_len : int
|
||||
Сколько позиций сгенерировать (обычно == входная длина x)
|
||||
start_pos : int, по умолчанию 0
|
||||
Возможность выдать positional embeddings \"с середины\" (для autoregressive генерации)
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
torch.Tensor — positional embeddings формы [seq_len, emb_size]
|
||||
|
||||
Пример:
|
||||
>>> pos_encoder = PositionalEmbeddings(100, 64)
|
||||
>>> emb = pos_encoder(10) # Тензор 10x64
|
||||
-------
|
||||
>>> pos = PositionalEmbeddings(512, 128)
|
||||
>>> p = pos(10) # [10, 128]
|
||||
"""
|
||||
if seq_len < 1 or seq_len > self.max_seq_len:
|
||||
raise IndexError(f"Длина {seq_len} должна быть от 1 до {self.max_seq_len}")
|
||||
if start_pos == 0:
|
||||
positions = torch.arange(seq_len, device=self.embedding.weight.device)
|
||||
else:
|
||||
positions = torch.arange(start=start_pos, end=start_pos + seq_len, device=self.embedding.weight.device)
|
||||
positions = torch.arange(
|
||||
start=start_pos,
|
||||
end=start_pos + seq_len,
|
||||
device=self.embedding.weight.device,
|
||||
)
|
||||
return self.embedding(positions)
|
||||
|
||||
123
llm/src/llm/core/rms_norm.py
Normal file
123
llm/src/llm/core/rms_norm.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
RMSNorm (Root Mean Square Normalization) - нормализация по среднеквадратичному значению.
|
||||
|
||||
Упрощенная версия LayerNorm без вычисления среднего значения. Широко используется
|
||||
в современных архитектурах типа LLaMA благодаря лучшей стабильности и производительности.
|
||||
|
||||
Научная статья: "Root Mean Square Layer Normalization"
|
||||
https://arxiv.org/abs/1910.07467
|
||||
|
||||
Формула:
|
||||
RMSNorm(x) = (x / RMS(x)) * w
|
||||
где RMS(x) = sqrt(mean(x²) + eps)
|
||||
|
||||
Преимущества:
|
||||
- Меньше вычислений (нет вычитания среднего)
|
||||
- Лучшая стабильность при обучении
|
||||
- Сохранение масштаба сигнала
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
"""
|
||||
RMSNorm (Root Mean Square Layer Normalization) — простая и эффективная альтернатива LayerNorm.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Нормализует входной тензор по последнему измерению только с помощью RMS (root mean square), без вычитания среднего.
|
||||
- Используется в LLaMA, PaLM и других крупных языковых моделях для лучшей стабильности и ускорения обучения.
|
||||
- В отличие от LayerNorm, не центрирует значения, что особенно полезно для автогерессивных трансформеров с residual-связями.
|
||||
|
||||
Мотивация и математика:
|
||||
-----------------------
|
||||
- Формула для одного слоя и вектора x:
|
||||
rms = sqrt( mean( x ** 2 ) + eps )
|
||||
out = w * ( x / rms )
|
||||
где w — learnable scale, eps — небольшая константа для численной устойчивости.
|
||||
- Нет смещения/вычитания среднего — сигнал сохраняет абсолютные значения, меньше “искажает” автоагрегатные значения на накопленных резидуалах.
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
dim : int
|
||||
Размер последнего нормализуемого измерения (обычно совпадает с размером embedding/final head).
|
||||
eps : float, default=1e-6
|
||||
Малое значение для устойчивости (additive epsilon).
|
||||
|
||||
Особенности:
|
||||
------------
|
||||
- Нет батч-нормализации, нет зависимости от размера батча.
|
||||
- Отлично подходит для больших моделей и автогерессии — меньше шуму от residual.
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> norm = RMSNorm(emb_size=256)
|
||||
>>> x = torch.randn(4, 10, 256)
|
||||
>>> out = norm(x) # возвращает tensor той же формы
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Zhang & Sennrich, "Root Mean Square Layer Normalization", 2019: https://arxiv.org/abs/1910.07467
|
||||
- Применение в LLaMA: https://arxiv.org/abs/2302.13971
|
||||
- HuggingFace implementation: https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/modeling_llama.py
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, dim: int, eps: float = 1e-6):
|
||||
"""
|
||||
Инициализация RMSNorm.
|
||||
|
||||
Args:
|
||||
-----
|
||||
dim : int
|
||||
Последнее нормализуемое измерение (обычно размерность embedding или hidden).
|
||||
eps : float
|
||||
Малое значение для устойчивости (по умолчанию 1e-6).
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Создаётся обучаемый scale weight w для каждой компоненты dim.
|
||||
- Сохраняется параметр eps для добавления к RMS.
|
||||
"""
|
||||
super().__init__()
|
||||
self._eps = eps
|
||||
self._w = nn.Parameter(torch.ones(dim))
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Прямой проход через RMSNorm.
|
||||
|
||||
Args:
|
||||
-----
|
||||
x : torch.Tensor
|
||||
Входной тензор любого shape с последней размерностью dim.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
torch.Tensor — тот же shape, что и вход x, но нормализованный по RMS на последнем измерении.
|
||||
|
||||
Алгоритм:
|
||||
---------
|
||||
- Вычислить rms = sqrt( mean( x**2, dim=-1, keepdim=True ) + eps )
|
||||
- Поделить x на rms
|
||||
- Помасштабировать обучаемым весом w
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> norm = RMSNorm(256)
|
||||
>>> out = norm(torch.randn(2, 10, 256))
|
||||
|
||||
"""
|
||||
# Вычисление RMS (Root Mean Square) по последнему измерению
|
||||
rms = (x.pow(2).mean(-1, keepdim=True) + self._eps) ** 0.5
|
||||
|
||||
# Нормализация и масштабирование
|
||||
norm_x = x / rms
|
||||
return self._w * norm_x
|
||||
|
||||
def extra_repr(self) -> str:
|
||||
"""Строковое представление для отладки."""
|
||||
return f"dim={self._w.shape[0]}, eps={self._eps}"
|
||||
197
llm/src/llm/core/rope.py
Normal file
197
llm/src/llm/core/rope.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Rotary Positional Embeddings (RoPE)
|
||||
===================================
|
||||
|
||||
Что такое RoPE?
|
||||
----------------
|
||||
RoPE — это способ "вписать" информацию о позиции токенов в скрытые вектора модели трансформера.
|
||||
Вместо простого сложения с абсолютным positional embedding, RoPE использует вращения векторов (как поворот стрелки на круге) внутри каждого attention head. Каждый элемент пары (вектор четного и нечетного индекса) поворачивается на угол, зависящий от позиции токена.
|
||||
|
||||
Зачем это?
|
||||
-----------
|
||||
- RoPE реализует **относительное позиционное кодирование**: модель может сравнивать "расстояния" между токенами, а не просто помнить положение.
|
||||
- Такое кодирование **улучшает генерацию длинных последовательностей** и перенос модели на тексты большей длины, чем были в обучении.
|
||||
- Форма векторов и длина (норма) НЕ искажаются.
|
||||
|
||||
Как это работает? (главная формула)
|
||||
-------------------------------------
|
||||
Для каждой позиции m и пары компонент (2i, 2i+1) внутри head применяются:
|
||||
|
||||
θ_i = base^(-2i / d)
|
||||
q'_{m,2i} = q_{m,2i} * cos(m * θ_i) - q_{m,2i+1} * sin(m * θ_i)
|
||||
q'_{m,2i+1} = q_{m,2i+1} * cos(m * θ_i) + q_{m,2i} * sin(m * θ_i)
|
||||
|
||||
где d — размерность "головы" attention (head_size), base обычно 10_000.
|
||||
|
||||
То есть, берём каждый "вектор" (в рамках head), делим на четные/нечетные части и поворачиваем их на уникальный угол, связанный с позицией/частотой.
|
||||
|
||||
Архитектурные детали:
|
||||
---------------------
|
||||
- Ваш тензор должен быть строго 4-мерным: [batch, num_heads, seq_len, head_size].
|
||||
- Размер head_size должен быть чётным!
|
||||
- RoPE применяется отдельно к **Q** и **K** в механизме внимания (но не к V).
|
||||
|
||||
Где об этом читать:
|
||||
-------------------
|
||||
- RoFormer: Enhanced Transformer with Rotary Position Embedding
|
||||
https://arxiv.org/abs/2104.09864
|
||||
- Llama: Open and Efficient Foundation Language Models
|
||||
https://arxiv.org/abs/2302.13971
|
||||
- Визуализация позиционных кодировок:
|
||||
https://kazemnejad.com/blog/transformer_architecture_positional_encoding/
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> rope = RoPE(head_size=64, max_seq_len=2048)
|
||||
>>> x = torch.randn(2, 8, 128, 64) # [batch, num_heads, seq_len, head_size]
|
||||
>>> x_enc = rope(x) # здесь вектор x обогатится позиционной информацией
|
||||
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RoPE(nn.Module):
|
||||
"""
|
||||
Реализация RoPE (Rotary Positional Embeddings) для self-attention в трансформерах.
|
||||
|
||||
Этот слой добавляет позиционную информацию к векторам внимания (Q, K) —
|
||||
не с помощью простого сложения с positional embedding, а с помощью математического
|
||||
вращения (как если бы вы крутили стрелку на круге) для каждой пары компонент
|
||||
(even/odd) в каждом attention head.
|
||||
|
||||
Формула (для каждого токена и каждой пары компонент внутри head):
|
||||
θ_i = base^(-2i / d)
|
||||
out_{m,2i} = x_{m,2i} * cos(m * θ_i) - x_{m,2i+1} * sin(m * θ_i)
|
||||
out_{m,2i+1} = x_{m,2i+1} * cos(m * θ_i) + x_{m,2i} * sin(m * θ_i)
|
||||
где d — head_size, base обычно 10_000, степень i по head axis.
|
||||
|
||||
Какие входы принимает:
|
||||
----------------------
|
||||
- x: обязательно размерности [batch, num_heads, seq_len, head_size]!
|
||||
- head_size (размер внимания) должен быть чётным.
|
||||
- start_pos: опционально, позволяет сдвигать позиционный offset для генерации с кэшем.
|
||||
|
||||
Что возвращает:
|
||||
---------------
|
||||
- Тот же тензор (x), только со встроенной позиционной информацией (“повёрнутый” RoPE-кодировкой).
|
||||
- Форма и тип выходного тензора не меняются.
|
||||
|
||||
Где используется:
|
||||
-----------------
|
||||
- В любых современных LLM (Llama, Mistral, GPT-NeoX и др.) для повышения устойчивости и generalization transformer's attention.
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> rope = RoPE(head_size=64, max_seq_len=2048)
|
||||
>>> x = torch.randn(2, 8, 128, 64) # (batch, num_heads, seq_len, head_size)
|
||||
>>> x_encoded = rope(x)
|
||||
|
||||
Подробнее про математику и примеры с визуализацией:
|
||||
---------------------------------------------------
|
||||
- RoFormer: https://arxiv.org/abs/2104.09864
|
||||
- Llama: https://arxiv.org/abs/2302.13971
|
||||
- Демонстрация наглядно: https://kazemnejad.com/blog/transformer_architecture_positional_encoding/
|
||||
"""
|
||||
|
||||
def __init__(self, head_size: int, max_seq_len: int, base: int = 10_000):
|
||||
"""
|
||||
Инициализация объекта RoPE — настраивает и предвычисляет все необходимые
|
||||
параметры для ротационного позиционного кодирования.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
head_size : int
|
||||
Размер одного attention head (последнего измерения вектора) — сколько компонент
|
||||
(float-значений) отвечает за одну "голову". Должен быть ЧЁТНЫМ числом, иначе RoPE не применим.
|
||||
Обычно head_size = embed_dim // num_heads.
|
||||
max_seq_len : int
|
||||
Максимальная длина последовательности, которую RoPE сможет обработать.
|
||||
Если ваш текст длиннее этого числа — будет ошибка! Например, для GPT2 обычно 1024, у LLaMA — до 4096.
|
||||
Это число определяет размер внутренних буферов cos/sin.
|
||||
base : int, по умолчанию 10_000
|
||||
База для вычисления частот вращения (θ_i) для каждой компоненты.
|
||||
В оригинальных статьях почти всегда используют base=10000.
|
||||
Менять этот параметр не нужно, если вы не исследуете математические детали.
|
||||
|
||||
Что происходит внутри:
|
||||
----------------------
|
||||
- Проверяется чётность head_size.
|
||||
- Для каждого возможного положения в пределах max_seq_len и каждой пары component высчитываются уникальные cos/sin значения (матрицы частот).
|
||||
- Эти матрицы используются далее для быстрого наложения позиционного "вращения" токенов внутри attention.
|
||||
"""
|
||||
super().__init__()
|
||||
assert head_size % 2 == 0, "head_size должен быть четным"
|
||||
|
||||
# Вычисление частот: θ_i = base^(-2i/d) для i ∈ [0, d/2-1]
|
||||
freqs = 1.0 / (base ** (2 * torch.arange(head_size // 2).float() / head_size))
|
||||
|
||||
# Позиции от 0 до max_seq_len-1
|
||||
positions = torch.arange(max_seq_len).float()
|
||||
|
||||
# Внешнее произведение: m * θ_i для всех позиций и частот
|
||||
freq_matrix = positions.unsqueeze(1) * freqs.unsqueeze(0)
|
||||
|
||||
# Предвычисление матриц косинусов и синусов
|
||||
self.register_buffer("cos_matrix", torch.cos(freq_matrix))
|
||||
self.register_buffer("sin_matrix", torch.sin(freq_matrix))
|
||||
|
||||
def forward(self, x: torch.Tensor, start_pos: int = 0) -> torch.Tensor:
|
||||
"""
|
||||
Применяет ротационное позиционное кодирование (RoPE) к входному тензору.
|
||||
|
||||
Что делает эта функция:
|
||||
-----------------------
|
||||
Для каждого токена в последовательности внутри каждого attention head
|
||||
"поворачивает" его вектор в подпространстве (even/odd пар) на свой уникальный угол,
|
||||
зависящий от позиции токена. Это позволяет attention "понимать расстояния" между токенами.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Входной тензор строго формы [batch, num_heads, seq_len, head_size].
|
||||
Это обычно либо Q, либо K из механизма внимания.
|
||||
start_pos : int, по умолчанию 0
|
||||
Сдвиг начала позиции (нужно при генерации с кэшем, почти всегда оставить 0 если не пишете автогенератор).
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
torch.Tensor с теми же формой и типом, что и x, но уже с наложенным позиционным кодированием.
|
||||
|
||||
Важно:
|
||||
-------
|
||||
- Если передан тензор не 4D, будет выброшено исключение!
|
||||
- Не изменяет значения "на месте", всегда возвращает новый тензор.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> rope = RoPE(head_size=64, max_seq_len=1024)
|
||||
>>> q = torch.randn(2, 8, 32, 64) # batch, num_heads, seq_len, head_size
|
||||
>>> q_rope = rope(q)
|
||||
"""
|
||||
assert x.ndim == 4, "RoPE поддерживает только 4D-вход [batch, num_heads, seq_len, head_size]"
|
||||
batch_size, num_heads, seq_len, head_size = x.shape
|
||||
|
||||
# Берем нужную часть матриц и приводим к типу x
|
||||
cos = self.cos_matrix[start_pos:start_pos+seq_len].to(x.dtype) # [seq_len, head_size//2]
|
||||
sin = self.sin_matrix[start_pos:start_pos+seq_len].to(x.dtype) # [seq_len, head_size//2]
|
||||
|
||||
# Явное изменение формы для broadcasting
|
||||
cos = cos.reshape(1, 1, seq_len, head_size // 2)
|
||||
sin = sin.reshape(1, 1, seq_len, head_size // 2)
|
||||
|
||||
# Разделяем на четные и нечетные компоненты по ПОСЛЕДНЕМУ измерению
|
||||
x_even = x[..., 0::2] # [batch_size, num_heads, seq_len, head_size//2]
|
||||
x_odd = x[..., 1::2] # [batch_size, num_heads, seq_len, head_size//2]
|
||||
|
||||
# Применяем поворот: q' = q * cos(mθ) + rotate(q) * sin(mθ)
|
||||
x_rotated_even = x_even * cos - x_odd * sin
|
||||
x_rotated_odd = x_even * sin + x_odd * cos
|
||||
|
||||
# Объединяем обратно в исходную размерность
|
||||
x_rotated = torch.stack([x_rotated_even, x_rotated_odd], dim=-1)
|
||||
x_rotated = x_rotated.flatten(-2) # [batch_size, seq_len, head_size]
|
||||
|
||||
return x_rotated
|
||||
70
llm/src/llm/core/silu.py
Normal file
70
llm/src/llm/core/silu.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class SiLU(nn.Module):
|
||||
"""
|
||||
SiLU (Sigmoid Linear Unit, также известная как Swish) — современная функция активации для нейросетей и LLM.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Формирует плавную нелинейную активацию: SiLU(x) = x * sigmoid(x).
|
||||
- Активно используется во всех новых архитектурах для больших языковых моделей (PaLM, LLaMA, Mistral, GPT-4 и др.).
|
||||
- Дает лучший поток градиентов по сравнению с ReLU, SELU, GELU в глубоких слоях — позволяет делать сети больше и глубже.
|
||||
|
||||
Мотивация и свойства:
|
||||
---------------------
|
||||
- SiLU объединяет свойства identity (для больших x) и ReLU (для отрицательных x, где есть затухание), но более плавно.
|
||||
- Позволяет проходить отрицательным значениям, а не "обрубает" как ReLU.
|
||||
- Better for optimization and training dynamics in deep LLMs, приводит к более богатым аппроксимациям.
|
||||
|
||||
Математическая формула:
|
||||
-----------------------
|
||||
SiLU(x) = x * sigmoid(x)
|
||||
где sigmoid(x) = 1 / (1 + exp(-x))
|
||||
|
||||
Сравнение с другими активациями:
|
||||
--------------------------------
|
||||
- ReLU(x): max(0, x) — простая отсечка
|
||||
- GELU(x): плавная вероятностная активация (используется в BERT/GPT-2)
|
||||
- SiLU(x): плавная альтернатива, часто лучше в современных LLM
|
||||
- Swish (Ramachandran et al., 2017) = SiLU
|
||||
|
||||
Args:
|
||||
-----
|
||||
Нет learnable параметров, чисто функциональная активация.
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> silu = SiLU()
|
||||
>>> x = torch.tensor([-2.0, 0.0, 2.0])
|
||||
>>> print(silu(x)) # тензор с элементами [-0.2384, 0.0, 1.7616] (примерно)
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Ramachandran et al., "Searching for Activation Functions", 2017: https://arxiv.org/abs/1710.05941
|
||||
- LLaMA: https://arxiv.org/abs/2302.13971
|
||||
- Swish в TensorFlow: https://arxiv.org/abs/1710.05941
|
||||
- Сравнение всех актив. функций: https://paperswithcode.com/method/silu
|
||||
"""
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
Применяет SiLU активацию ко всем компонентам тензора (x * sigmoid(x)).
|
||||
|
||||
Args:
|
||||
-----
|
||||
x : torch.Tensor
|
||||
Входной тензор любой формы.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
torch.Tensor — тензор той же формы, каждый элемент преобразован по формуле SiLU(x).
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> silu = SiLU()
|
||||
>>> x = torch.linspace(-3, 3, 7)
|
||||
>>> y = silu(x)
|
||||
"""
|
||||
return torch.sigmoid(x) * x
|
||||
130
llm/src/llm/core/swi_glu.py
Normal file
130
llm/src/llm/core/swi_glu.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
SwiGLU (Swish-Gated Linear Unit) - активационная функция с gating mechanism.
|
||||
|
||||
Комбинация Swish активации и Gating Linear Unit. Широко используется в современных
|
||||
моделях типа LLaMA и PaLM благодаря улучшенной производительности.
|
||||
|
||||
Научная статья: "GLU Variants Improve Transformer"
|
||||
https://arxiv.org/abs/2002.05202
|
||||
|
||||
Формула:
|
||||
SwiGLU(x) = Swish(xW_g + b_g) ⊙ (xW_u + b_u) * W_d + b_d
|
||||
|
||||
Преимущества:
|
||||
- Лучшая производительность чем у ReLU/GELU
|
||||
- Gating mechanism позволяет модели лучше выбирать информацию
|
||||
- Хорошо масштабируется для больших моделей
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from typing import Optional
|
||||
from .silu import SiLU
|
||||
|
||||
|
||||
class SwiGLU(nn.Module):
|
||||
"""
|
||||
SwiGLU (Swish-Gated Linear Unit) — эффективная feed-forward нелинейность для трансформеров (LLAMA, PaLM, Mistral).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Улучшает классический блок FeedForward (FFN) в трансформерах за счёт \"gating\" (механизма управления информационным потоком).
|
||||
- Использует нелинейность SiLU (Swish) вместо ReLU или GELU, повышая capacity блока.
|
||||
- Является дефолтом во всех современных LLM, начиная с PaLM, LLaMA и Mistral.
|
||||
|
||||
Формула и математика:
|
||||
---------------------
|
||||
Пусть x — вход, then:
|
||||
|
||||
SwiGLU(x) = (SiLU(W_g x + b_g)) ⊙ (W_u x + b_u) W_d + b_d
|
||||
|
||||
Типовая реализация (как здесь, по LLAMA/Mistral):
|
||||
gate = SiLU(Linear_gate(x)) # фитчерный \"gate\"
|
||||
up = Linear_up(x) # пропускная ветка
|
||||
mult = gate * up # поэлементное умножение (контроль информации)
|
||||
out = Linear_down(mult) # финальная проекция
|
||||
out = Dropout(out) # регуляризация
|
||||
|
||||
Почему это работает:
|
||||
-------------------
|
||||
- Gating позволяет информации проходить \"частично\", динамически подавляя/усиливая сигналы в hidden-space.
|
||||
- SiLU обеспечивает smooth градиенты (лучше для обучения LLM).
|
||||
- В экспериментах (PaLM, LLAMA) SwiGLU consistently outperforms ReLU, GELU, обычные GLU.
|
||||
|
||||
Параметры конструктора:
|
||||
-----------------------
|
||||
emb_size: int
|
||||
Размерность входного (и выходного) признакового пространства.
|
||||
dropout: float
|
||||
Dropout после final linear (обычно около 0.1).
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> block = SwiGLU(emb_size=512, dropout=0.1)
|
||||
>>> x = torch.randn(8, 16, 512)
|
||||
>>> y = block(x)
|
||||
>>> print(y.shape) # torch.Size([8, 16, 512])
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Shazeer, \"GLU Variants Improve Transformer\", 2020: https://arxiv.org/abs/2002.05202
|
||||
- PaLM: https://arxiv.org/abs/2204.02311 (Section 4.1)
|
||||
- LLaMA: https://arxiv.org/abs/2302.13971
|
||||
- Mistral: https://arxiv.org/abs/2310.06825
|
||||
- HuggingFace discussion: https://huggingface.co/docs/transformers/main/en/model_doc/llama
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, emb_size: int, dropout: float = 0.1):
|
||||
"""
|
||||
Инициализация SwiGLU слоя.
|
||||
|
||||
Args:
|
||||
emb_size: Размерность входных/выходных эмбеддингов
|
||||
dropout: Вероятность dropout (по умолчанию 0.1)
|
||||
"""
|
||||
super().__init__()
|
||||
self._gate = nn.Linear(emb_size, 4 * emb_size)
|
||||
self._up = nn.Linear(emb_size, 4 * emb_size)
|
||||
self._down = nn.Linear(4 * emb_size, emb_size)
|
||||
self._activation = SiLU()
|
||||
self._dropout = nn.Dropout(dropout)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Прямой проход через блок SwiGLU.
|
||||
|
||||
Args:
|
||||
-----
|
||||
x : torch.Tensor
|
||||
Входной тензор формы [batch_size, seq_len, emb_size]
|
||||
|
||||
Returns:
|
||||
--------
|
||||
torch.Tensor той же формы
|
||||
|
||||
Алгоритм:
|
||||
---------
|
||||
1. gate = SiLU(linear_gate(x))
|
||||
2. up = linear_up(x)
|
||||
3. mult = gate * up # поэлементно
|
||||
4. out = linear_down(mult)
|
||||
5. out = dropout(out)
|
||||
"""
|
||||
# Gate ветвь: линейное преобразование + активация
|
||||
gate_out = self._gate(x) # [batch, seq, 4*emb]
|
||||
activation_out = self._activation(gate_out) # [batch, seq, 4*emb]
|
||||
|
||||
# Up ветвь: линейное преобразование
|
||||
up_out = self._up(x) # [batch, seq, 4*emb]
|
||||
|
||||
# Element-wise multiplication (gating mechanism)
|
||||
out = up_out * activation_out # поэлементное умножение!
|
||||
|
||||
# Final projection and dropout
|
||||
out = self._down(out) # [batch, seq, emb]
|
||||
return self._dropout(out)
|
||||
|
||||
def extra_repr(self) -> str:
|
||||
"""Строковое представление для отладки."""
|
||||
return f"emb_size={self._gate.in_features}, dropout={self._dropout.p}"
|
||||
@@ -2,67 +2,96 @@ import torch
|
||||
from torch import nn
|
||||
from torch import Tensor
|
||||
|
||||
|
||||
class TokenEmbeddings(nn.Module):
|
||||
"""
|
||||
Модуль PyTorch для преобразования индексов токенов в векторные представления (эмбеддинги).
|
||||
|
||||
Преобразует целочисленные индексы токенов в обучаемые векторные представления фиксированного размера.
|
||||
Обычно используется как первый слой в нейронных сетях для задач NLP.
|
||||
|
||||
Аргументы:
|
||||
vocab_size (int): Размер словаря (количество уникальных токенов)
|
||||
emb_size (int): Размерность векторных представлений
|
||||
|
||||
Форматы данных:
|
||||
- Вход: тензор (batch_size, seq_len) индексов токенов
|
||||
- Выход: тензор (batch_size, seq_len, emb_size) векторных представлений
|
||||
|
||||
Примеры использования:
|
||||
>>> embedding_layer = TokenEmbeddings(vocab_size=10000, emb_size=256)
|
||||
>>> tokens = torch.tensor([[1, 2, 3], [4, 5, 6]]) # batch_size=2, seq_len=3
|
||||
>>> embeddings = embedding_layer(tokens)
|
||||
>>> embeddings.shape
|
||||
torch.Size([2, 3, 256])
|
||||
|
||||
Примечание:
|
||||
- Индексы должны быть в диапазоне [0, vocab_size-1]
|
||||
- Эмбеддинги инициализируются случайно и обучаются в процессе тренировки модели
|
||||
TokenEmbeddings — обучаемый слой эмбеддингов для токенов (слов, сабслов, байтов и т.д.) в трансформерах.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Преобразует каждый целочисленный индекс-токен из словаря (vocab) в обучаемый dense-вектор фиксированной длины.
|
||||
- Это "входной слой" для любой нейросетевой языковой модели: позволяет работать с текстом как с матрицей чисел, а не с индексами/категориальными значениями.
|
||||
- Обеспечивает возможность end-to-end обучения embedding-матрицы совместно с целью модели.
|
||||
|
||||
Мотивация и особенности:
|
||||
------------------------
|
||||
- Каждый токен (индекс) получает свой learnable embedding (float-вектор).
|
||||
- Размерность слоя: [vocab_size, emb_size] (матрица эмбеддингов).
|
||||
- Веса эмбеддингов инициализируются случайно и обучаются вместе с остальной моделью.
|
||||
- Аналог таблицы эмбеддингов в word2vec/fastText, но управляется end-to-end.
|
||||
- Могут использоваться с любым токенизатором (BPE, SentencePiece, WordPiece и др.).
|
||||
|
||||
Формула:
|
||||
--------
|
||||
emb(x) = W[x], где W — матрица размера [vocab_size, emb_dim], x — индексы shape [batch, seq_len]
|
||||
На выходе: тензор [batch, seq_len, emb_dim]
|
||||
|
||||
Args:
|
||||
-----
|
||||
vocab_size: int — размер словаря/алфавита (количество уникальных токенов)
|
||||
emb_size: int — размерность (длина) эмбеддинговых векторов (обычно 256/512/1024...)
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> embedding = TokenEmbeddings(vocab_size=5000, emb_size=256)
|
||||
>>> tokens = torch.tensor([[12, 47, 301], [6, 88, 413]])
|
||||
>>> vecs = embedding(tokens)
|
||||
>>> print(vecs.shape) # torch.Size([2, 3, 256])
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Mikolov et al., "Efficient Estimation of Word Representations in Vector Space (word2vec)", 2013
|
||||
- Vaswani et al., "Attention is All You Need", 2017: https://arxiv.org/abs/1706.03762
|
||||
- BPE, SentencePiece overviews: https://huggingface.co/docs/transformers/tokenizer_summary
|
||||
"""
|
||||
|
||||
def __init__(self, vocab_size: int, emb_size: int):
|
||||
"""
|
||||
Инициализация слоя эмбеддингов.
|
||||
|
||||
Args:
|
||||
-----
|
||||
vocab_size: int
|
||||
Размер словаря (уникальных токенов/индексов).
|
||||
emb_size: int
|
||||
Длина эмбеддингового вектора для каждого токена.
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Создаёт nn.Embedding с [vocab_size, emb_size] learnable весами.
|
||||
"""
|
||||
super().__init__()
|
||||
self._embedding = nn.Embedding(
|
||||
num_embeddings=vocab_size,
|
||||
embedding_dim=emb_size
|
||||
num_embeddings=vocab_size, embedding_dim=emb_size
|
||||
)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""
|
||||
Получить эмбеддинги для входных токенов.
|
||||
|
||||
Args:
|
||||
-----
|
||||
x : torch.Tensor
|
||||
Тензор shape [...], содержащий индексы токенов (каждое значение от 0 до vocab_size-1).
|
||||
|
||||
Returns:
|
||||
--------
|
||||
torch.Tensor — тензор обычной формы [..., emb_size] (на каждую позицию — свой embedding-вектор).
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> embedding = TokenEmbeddings(vocab_size=100, emb_size=64)
|
||||
>>> tokens = torch.tensor([[0, 99, 5]])
|
||||
>>> vecs = embedding(tokens) # [1, 3, 64]
|
||||
"""
|
||||
return self._embedding(x)
|
||||
|
||||
@property
|
||||
def num_embeddings(self) -> int:
|
||||
"""Возвращает размер словаря"""
|
||||
"""Возвращает размер словаря (количество уникальных токенов)."""
|
||||
return self._embedding.num_embeddings
|
||||
|
||||
@property
|
||||
def embedding_dim(self) -> int:
|
||||
"""Возвращает размерность эмбеддингов"""
|
||||
"""Возвращает размерность эмбеддингов (длина вектора каждого токена)."""
|
||||
return self._embedding.embedding_dim
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Пример использования
|
||||
embedding = TokenEmbeddings(vocab_size=100, emb_size=128)
|
||||
|
||||
# Создаем тензор с индексами в пределах vocab_size (0-99)
|
||||
tensor = torch.tensor([
|
||||
[11, 45, 76, 34],
|
||||
[34, 67, 45, 54]
|
||||
])
|
||||
|
||||
# Проверяем индексы
|
||||
if (tensor >= 100).any():
|
||||
raise ValueError("Some indices are out of vocabulary range (vocab_size=100)")
|
||||
|
||||
output = embedding(tensor)
|
||||
print("Embeddings shape:", output.shape)
|
||||
print(f"{output.shape} | {output.mean().item():.11f}") # Формат как в ТЗ
|
||||
0
llm/src/llm/datasets/__init__.py
Normal file
0
llm/src/llm/datasets/__init__.py
Normal file
120
llm/src/llm/datasets/streaming_text_dataset.py
Normal file
120
llm/src/llm/datasets/streaming_text_dataset.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from typing import List, Any
|
||||
|
||||
|
||||
class StreamingTextDataset(Dataset):
|
||||
"""
|
||||
StreamingTextDataset — потоковый датасет для LLM/NLP на базе списка строк.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Позволяет эффективно обрабатывать большие текстовые выборки, итерируя по заранее подготовленному списку строк.
|
||||
- При итерации строки токенизируются на лету, превращаются в примеры фиксированной длины block_size (padding/truncation внутри класса).
|
||||
- Поддерживает стандартный DataLoader PyTorch.
|
||||
|
||||
Ключевые особенности:
|
||||
---------------------
|
||||
- Не требует загрузки всей коллекции токенов в RAM: поддерживает работу с любым размером датасета, если список строк заранее подготовлен.
|
||||
- Каждый пример (sample) формируется при обращении; не хранит массив батчей, не использует файлы внутри.
|
||||
- Поддерживает любой токенизатор с методом encode (например, BPE, SentencePiece, HF Tokenizer).
|
||||
- batch_size и параллелизм (num_workers) контролируются через DataLoader.
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
texts: List[str] — список строк (предварительно загруженных обучающих примеров).
|
||||
tokenizer: BaseTokenizer/Any — объект с методом encode(str, **kwargs) -> List[int].
|
||||
block_size: int — длина одного выходного примера в токенах (padding/truncation если нужно).
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> texts = open("wiki_sample.txt", encoding="utf-8").read().splitlines()
|
||||
>>> ds = StreamingTextDataset(texts, tokenizer=tokenizer, block_size=512)
|
||||
>>> loader = torch.utils.data.DataLoader(ds, batch_size=8)
|
||||
>>> for batch in loader:
|
||||
... print(batch['input_ids'].shape) # torch.Size([8, 512])
|
||||
|
||||
Особенности:
|
||||
------------
|
||||
- Проектирован для бесконечного стриминга текстовых данных из больших коллекций.
|
||||
- При batch_size > 1 каждый batch формируется DataLoader-ом из yield'ов этого датасета.
|
||||
- Не работает с файлами напрямую, только со строками (списком).
|
||||
- Подходит для обучения LLM, тестирования, дообучения, оценки на больших потоковых данных.
|
||||
|
||||
References:
|
||||
-----------
|
||||
- PyTorch IterableDataset: https://pytorch.org/docs/stable/data.html#torch.utils.data.IterableDataset
|
||||
- HuggingFace streaming datasets: https://huggingface.co/docs/datasets/stream
|
||||
- Практика масштабного обучения LLM: https://github.com/karpathy/nanoGPT/issues/182
|
||||
"""
|
||||
|
||||
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128):
|
||||
"""
|
||||
Инициализация StreamingTextDataset из списка строк.
|
||||
|
||||
Аргументы:
|
||||
texts (List[str]): Список строк — текстовые обучающие примеры; весь датасет должен помещаться в этот список.
|
||||
tokenizer (Any): Токенизатор с методом encode(text, **kwargs) -> List[int].
|
||||
block_size (int, по умолчанию 128): Желаемая длина токенизированного примера (padding/truncation внутри класса).
|
||||
|
||||
Особенности:
|
||||
- Поддерживает итеративную загрузку, эффективен для больших текстовых выборок.
|
||||
- Каждый пример автоматически дополняется или усекается до block_size.
|
||||
- Не читает данные из файла/буфера, а только из заранее подготовленного списка строк.
|
||||
|
||||
Пример:
|
||||
>>> ds = StreamingTextDataset(texts=all_lines, tokenizer=tokenizer, block_size=256)
|
||||
>>> for ex in ds:
|
||||
... print(ex['input_ids'].shape) # torch.Size([256])
|
||||
"""
|
||||
self.texts = texts
|
||||
self.tokenizer = tokenizer
|
||||
self.block_size = block_size
|
||||
|
||||
# Получаем pad_token_id из токенизатора
|
||||
self.pad_token_id = getattr(tokenizer, "pad_token_id", 0)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Возвращает количество доступных примеров в датасете.
|
||||
|
||||
Returns:
|
||||
int: Число примеров (равно длине исходного списка строк).
|
||||
"""
|
||||
return len(self.texts)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""
|
||||
Получить обработанный пример по индексу из потокового датасета.
|
||||
|
||||
Аргументы:
|
||||
idx (int): Индекс примера в исходном списке строк.
|
||||
|
||||
Возвращает:
|
||||
dict: Словарь с тензорами для обучения LLM:
|
||||
- 'input_ids': torch.Tensor формы [block_size] — индексы токенов (padding/truncation выполнены)
|
||||
- 'labels': torch.Tensor формы [block_size] — целевые метки (обычно совпадают с input_ids)
|
||||
|
||||
Пример:
|
||||
>>> item = dataset[10]
|
||||
>>> assert isinstance(item, dict)
|
||||
>>> assert item['input_ids'].shape == (block_size,)
|
||||
>>> assert 'labels' in item
|
||||
"""
|
||||
text = self.texts[idx]
|
||||
|
||||
# Токенизация на лету
|
||||
input_ids = self.tokenizer.encode(text, add_special_tokens=False)
|
||||
|
||||
# Обрезаем или дополняем до нужной длины
|
||||
if len(input_ids) > self.block_size:
|
||||
input_ids = input_ids[: self.block_size]
|
||||
else:
|
||||
input_ids = input_ids + [self.pad_token_id] * (
|
||||
self.block_size - len(input_ids)
|
||||
)
|
||||
|
||||
input_ids = torch.tensor(input_ids, dtype=torch.long)
|
||||
labels = input_ids.clone()
|
||||
|
||||
return {"input_ids": input_ids, "labels": labels}
|
||||
112
llm/src/llm/datasets/text_dataset.py
Normal file
112
llm/src/llm/datasets/text_dataset.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from typing import List, Any
|
||||
|
||||
|
||||
class TextDataset(Dataset):
|
||||
"""
|
||||
TextDataset — простой датасет для подачи обучающих токенов LLM (batch-режим или по одному примеру).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Хранит последовательности текста (каждую строку или пример) в виде списка строк.
|
||||
- При обращении сам токенизирует строку в последовательность индексов с помощью заданного токенизатора.
|
||||
- Каждый пример автоматически усекётся или будет дополнен до фиксированной длины block_size (padding — zeros).
|
||||
|
||||
Формат и аргументы конструктора:
|
||||
-------------------------------
|
||||
texts: List[str]
|
||||
Список строк, каждая из которых рассматривается как отдельный обучающий пример.
|
||||
tokenizer: любой объект с методом encode(str, **kwargs) → List[int]
|
||||
Обеспечивает сопоставление строки списку токенов (например, BPE, HuggingFace, SentencePiece и др.).
|
||||
block_size: int, по умолчанию 128
|
||||
Желаемая длина выходной последовательности (padding/truncation внутри класса).
|
||||
|
||||
Особенности:
|
||||
------------
|
||||
- Класс не работает с файлами напрямую: данные передаются готовым списком строк.
|
||||
- При недостаточной длине пример дополняется паддингом (нулём или другим токеном, зависит от реализации).
|
||||
- Может возвращать dict с input_ids, labels и прочими ключами (см. реализацию в функции __getitem__).
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> with open("dataset.txt", encoding="utf-8") as f:
|
||||
... texts = f.read().splitlines()
|
||||
>>> dataset = TextDataset(texts, tokenizer, block_size=256)
|
||||
>>> from torch.utils.data import DataLoader
|
||||
>>> loader = DataLoader(dataset, batch_size=4)
|
||||
>>> for item in loader:
|
||||
... # item['input_ids'] для обучения LLM
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Torch Dataset: https://pytorch.org/docs/stable/data.html
|
||||
- Примеры LLM датасетов в open-source: https://github.com/karpathy/nanoGPT/blob/master/data/shakespeare_char/tokenize.py
|
||||
"""
|
||||
|
||||
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128):
|
||||
"""
|
||||
Инициализация датасета из списка строк.
|
||||
|
||||
Аргументы:
|
||||
texts (List[str]): Список строк — каждый элемент отдельный обучающий пример.
|
||||
tokenizer (Any): Токенизатор с методом encode(str, **kwargs) -> List[int].
|
||||
block_size (int, по умолчанию 128): Желаемая длина результата —
|
||||
длинные последовательности будут усечены, короткие — дополнены паддингом (pad_token_id или 0).
|
||||
|
||||
Особенности:
|
||||
- Строки не фильтруются и не изменяются внутри датасета.
|
||||
- Для PAD используется pad_token_id из токенизатора (если есть) либо 0.
|
||||
- Dict, возвращаемый __getitem__, содержит 'input_ids' и 'labels'.
|
||||
|
||||
Пример:
|
||||
>>> dataset = TextDataset(["hello world", "test string"], tokenizer, block_size=16)
|
||||
"""
|
||||
self.examples = []
|
||||
self.tokenizer = tokenizer
|
||||
self.block_size = block_size
|
||||
|
||||
for text in texts:
|
||||
# Кодируем текст в токены
|
||||
input_ids = tokenizer.encode(text, add_special_tokens=False)
|
||||
|
||||
# Обрезаем или дополняем до нужной длины
|
||||
if len(input_ids) > block_size:
|
||||
input_ids = input_ids[:block_size]
|
||||
else:
|
||||
# Дополняем pad_token_id
|
||||
pad_token_id = getattr(tokenizer, "pad_token_id", 0)
|
||||
input_ids = input_ids + [pad_token_id] * (block_size - len(input_ids))
|
||||
|
||||
self.examples.append(input_ids)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Возвращает количество примеров в датасете (длина списка текстов).
|
||||
|
||||
Returns:
|
||||
int: Число примеров в датасете.
|
||||
"""
|
||||
return len(self.examples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""
|
||||
Получить пример из датасета по индексу.
|
||||
|
||||
Аргументы:
|
||||
idx (int): Индекс примера.
|
||||
|
||||
Возвращает:
|
||||
dict: Словарь с тензорами токенов для модели:
|
||||
- 'input_ids': torch.Tensor shape [block_size], индексы токенов для входа.
|
||||
- 'labels': torch.Tensor shape [block_size], метки для LM задачи (обычно совпадают с input_ids).
|
||||
|
||||
Пример:
|
||||
>>> item = dataset[7]
|
||||
>>> assert isinstance(item, dict)
|
||||
>>> assert item['input_ids'].shape == (block_size,)
|
||||
>>> assert 'labels' in item
|
||||
"""
|
||||
input_ids = torch.tensor(self.examples[idx], dtype=torch.long)
|
||||
labels = input_ids.clone()
|
||||
return {"input_ids": input_ids, "labels": labels}
|
||||
124
llm/src/llm/datasets/text_with_special_tokens_dataset.py
Normal file
124
llm/src/llm/datasets/text_with_special_tokens_dataset.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from typing import List, Any
|
||||
from llm.datasets.text_dataset import TextDataset
|
||||
|
||||
|
||||
class TextWithSpecialTokensDataset(TextDataset):
|
||||
"""
|
||||
TextWithSpecialTokensDataset — датасет для языковых моделей с поддержкой специальных токенов (BOS, EOS, PAD).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Работает с уже готовым списком строк (не с файлом!).
|
||||
- Токенизирует строки с помощью заданного токенизатора, вручную вставляет специальные токены (BOS/ EOS/ PAD).
|
||||
- Обрезает или дополняет каждую последовательность до длины block_size.
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
texts (List[str]): Список обучающих строк (примеров).
|
||||
tokenizer (Any): Любой токенизатор с методом encode(text, **kwargs).
|
||||
block_size (int, default=128): Желаемая длина примера (padding/truncation).
|
||||
add_bos (bool, default=False): Если True, добавляет BOS-токен в начало каждой последовательности.
|
||||
add_eos (bool, default=False): Если True, добавляет EOS-токен в конец.
|
||||
|
||||
Особенности:
|
||||
------------
|
||||
- Если pad_token_id не задан — по умолчанию паддит нулями.
|
||||
- Все returned примеры — dict с 'input_ids' и 'labels' (shape == block_size).
|
||||
- Обрезание/дополнение учётное: BOS/EOS не "выдавливаются" обрезкой.
|
||||
- Пример вызова:
|
||||
>>> texts = ["пример текста", "ещё текст"]
|
||||
>>> ds = TextWithSpecialTokensDataset(texts, tokenizer, block_size=16, add_bos=True, add_eos=True)
|
||||
>>> out = ds[0]
|
||||
>>> assert out['input_ids'].shape == (16,)
|
||||
|
||||
References:
|
||||
-----------
|
||||
- OpenAI GPT-2 data loader: https://github.com/openai/gpt-2/blob/master/src/encode.py
|
||||
- HuggingFace data docs: https://huggingface.co/docs/transformers/pad_truncation
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
texts: List[str],
|
||||
tokenizer: Any,
|
||||
block_size: int = 128,
|
||||
add_bos: bool = False,
|
||||
add_eos: bool = False,
|
||||
):
|
||||
"""
|
||||
Инициализация датасета с поддержкой специальных токенов.
|
||||
|
||||
Args:
|
||||
texts (List[str]): Список строк (все ваши обучающие примеры).
|
||||
tokenizer (Any): Токенизатор с методом encode(text, **kwargs).
|
||||
block_size (int): Длина выходного примера.
|
||||
add_bos (bool): Добавлять ли BOS токен в начало.
|
||||
add_eos (bool): Добавлять ли EOS токен в конец.
|
||||
"""
|
||||
self.examples = []
|
||||
self.tokenizer = tokenizer
|
||||
self.block_size = block_size
|
||||
self.add_bos = add_bos
|
||||
self.add_eos = add_eos
|
||||
|
||||
for text in texts:
|
||||
# Кодируем с специальными токенами
|
||||
input_ids = tokenizer.encode(
|
||||
text, add_special_tokens=True, add_bos_token=add_bos, add_eos_token=add_eos
|
||||
)
|
||||
|
||||
# Учитываем специальные токены при обрезке/дополнении
|
||||
effective_block_size = block_size
|
||||
if add_bos:
|
||||
effective_block_size -= 1
|
||||
if add_eos:
|
||||
effective_block_size -= 1
|
||||
|
||||
if len(input_ids) > effective_block_size:
|
||||
input_ids = input_ids[:effective_block_size]
|
||||
|
||||
# Добавляем специальные токены если нужно
|
||||
if (
|
||||
add_bos
|
||||
and hasattr(tokenizer, "bos_token_id")
|
||||
and tokenizer.bos_token_id is not None
|
||||
):
|
||||
input_ids = [tokenizer.bos_token_id] + input_ids
|
||||
if (
|
||||
add_eos
|
||||
and hasattr(tokenizer, "eos_token_id")
|
||||
and tokenizer.eos_token_id is not None
|
||||
):
|
||||
input_ids = input_ids + [tokenizer.eos_token_id]
|
||||
|
||||
# Дополняем до полной длины
|
||||
pad_token_id = getattr(tokenizer, "pad_token_id", 0)
|
||||
if len(input_ids) < block_size:
|
||||
input_ids = input_ids + [pad_token_id] * (block_size - len(input_ids))
|
||||
|
||||
self.examples.append(input_ids)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Возвращает количество примеров в датасете.
|
||||
|
||||
Returns:
|
||||
int: Размер (len(self.examples)).
|
||||
"""
|
||||
return len(self.examples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""
|
||||
Получить пример с учётом специальных токенов и паддинга.
|
||||
|
||||
Args:
|
||||
idx (int): Индекс в dataset.
|
||||
|
||||
Returns:
|
||||
dict: {'input_ids': torch.Tensor [block_size], 'labels': torch.Tensor [block_size]}
|
||||
"""
|
||||
input_ids = torch.tensor(self.examples[idx], dtype=torch.long)
|
||||
labels = input_ids.clone()
|
||||
return {"input_ids": input_ids, "labels": labels}
|
||||
@@ -1,200 +1,252 @@
|
||||
# llm/models/gpt/gpt2.py
|
||||
"""
|
||||
Классическая GPT (Generative Pre-trained Transformer), OpenAI 2018.
|
||||
|
||||
Научная суть:
|
||||
- Первая массовая архитектура языка на основе исключительно self-attention механизмов (трансформер-декодер).
|
||||
- Обучается сначала на задаче языкового моделирования (unsupervised), далее дообучается на downstream-задачах (transfer learning).
|
||||
- Обеспечивает длинную память и “глобальный” контекст благодаря attention.
|
||||
|
||||
Ключевые элементы:
|
||||
- masked self-attention (causal)
|
||||
- LayerNorm ПОСЛЕ attention и FFN (что отличает от GPT2)
|
||||
- GELU активация
|
||||
- Absolute learned positional embeddings
|
||||
|
||||
Подробнее: Radford et al., "Improving Language Understanding by Generative Pre-Training", arXiv:1801.10198
|
||||
https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf
|
||||
|
||||
Пример использования:
|
||||
>>> model = GPT({"vocab_size": 50257, ...})
|
||||
>>> logits = model(input_ids)
|
||||
>>> out = model.generate(input_ids, max_length=30)
|
||||
"""
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from typing import Optional, Dict
|
||||
from llm.core.base_model import BaseModel
|
||||
from llm.core.decoder import Decoder
|
||||
from llm.core.token_embeddings import TokenEmbeddings
|
||||
from llm.core.positional_embeddings import PositionalEmbeddings
|
||||
|
||||
|
||||
class GPT(BaseModel):
|
||||
"""GPT-like трансформер для генерации текста
|
||||
|
||||
Args:
|
||||
vocab_size: Размер словаря
|
||||
max_seq_len: Макс. длина последовательности
|
||||
emb_size: Размерность эмбеддингов
|
||||
num_heads: Количество голов внимания
|
||||
head_size: Размерность голов внимания
|
||||
num_layers: Количество слоёв декодера
|
||||
dropout: Вероятность dropout (default=0.1)
|
||||
device: Устройство (default='cpu')
|
||||
"""
|
||||
GPT (Generative Pretrained Transformer) — автогерессивная языковая модель по мотивам оригинального GPT/GPT-2 architecture.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Позволяет предсказывать и генерировать последовательности текста, обучаясь на задаче language modeling (предсказывать следующий токен).
|
||||
- Класс реализует архитектуру classic Transformer Decoder Stack с masked multi-head attention и token/positional embeddings.
|
||||
- Используется как базовая модель для генерации, zero-/few-shot, задач обучения с подкреплением и пр.
|
||||
|
||||
Архитектурные особенности:
|
||||
--------------------------
|
||||
- Embedding-слои для токенов (token_embeddings) и позиций (position_embeddings).
|
||||
- Stack из N декодер-блоков (MultiHeadAttention + FeedForward + residual + LayerNorm).
|
||||
- Masked self-attention — каждый токен видит только свои и предыдущие, обеспечивая автогерессию.
|
||||
- LayerNorm до проекции на словарь (pre-LN).
|
||||
- Поддержка efficient KV кэша — ускоряет autoregressive inference/generation.
|
||||
|
||||
Основные параметры:
|
||||
-------------------
|
||||
config: dict в формате {
|
||||
vocab_size, # размер словаря токенов
|
||||
embed_dim, # размерность эмбеддинга
|
||||
num_heads, # количество attention heads
|
||||
num_layers, # глубина модели (число блоков)
|
||||
max_position_embeddings,
|
||||
dropout
|
||||
}
|
||||
|
||||
Формула и поток данных:
|
||||
-----------------------
|
||||
x -> token_embeddings -> + position_embeddings -> dropout ->
|
||||
-> stack([DecoderBlock]) ->
|
||||
-> LayerNorm ->
|
||||
-> Linear(out_dim=vocab_size) -> output_logits
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> gpt = GPT({...})
|
||||
>>> tokens = torch.tensor([[12, 123, 44]])
|
||||
>>> logits = gpt(tokens)
|
||||
>>> generated = gpt.generate(tokens, max_new_tokens=10)
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Radford et al., "Improving Language Understanding by Generative Pre-Training" (GPT-1, 2018)
|
||||
https://cdn.openai.com/research-covers/languageunsupervised/language_understanding_paper.pdf
|
||||
- Original BPE Tokenizer code: https://github.com/openai/gpt-2/blob/master/src/encoder.py
|
||||
- Формула masked self-attention: Vaswani et al., "Attention is All You Need", 2017
|
||||
https://arxiv.org/abs/1706.03762
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Инициализация модели GPT.
|
||||
|
||||
Args:
|
||||
-----
|
||||
config: dict
|
||||
Параметры архитектуры:
|
||||
vocab_size: int — размер словаря токенов
|
||||
embed_dim: int — размерность эмбеддинга
|
||||
num_heads: int — количество attention-heads
|
||||
num_layers: int — число Transformer блоков
|
||||
max_position_embeddings: int — макс. длина последовательности
|
||||
dropout: float — dropout
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Создаёт слой эмбеддингов, позиционку, стек декодеров, нормализацию, линейную проекцию.
|
||||
"""
|
||||
super().__init__(config)
|
||||
|
||||
# Инициализация слоев
|
||||
self._max_seq_len = config["max_position_embeddings"]
|
||||
self._token_embeddings = TokenEmbeddings(
|
||||
vocab_size=config["vocab_size"],
|
||||
emb_size=config["embed_dim"]
|
||||
vocab_size=config["vocab_size"], emb_size=config["embed_dim"]
|
||||
)
|
||||
self._position_embeddings = PositionalEmbeddings(
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
emb_size=config["embed_dim"]
|
||||
max_seq_len=config["max_position_embeddings"], emb_size=config["embed_dim"]
|
||||
)
|
||||
self._dropout = nn.Dropout(config["dropout"])
|
||||
# head_size = emb_size // num_heads
|
||||
self._decoders = nn.ModuleList([Decoder(
|
||||
num_heads=config["num_heads"],
|
||||
emb_size=config["embed_dim"],
|
||||
head_size=config["embed_dim"] // config["num_heads"],
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
dropout=config["dropout"]
|
||||
) for _ in range(config["num_layers"])])
|
||||
self._decoders = nn.ModuleList(
|
||||
[
|
||||
Decoder(
|
||||
num_heads=config["num_heads"],
|
||||
emb_size=config["embed_dim"],
|
||||
head_size=config["embed_dim"] // config["num_heads"],
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
dropout=config["dropout"],
|
||||
)
|
||||
for _ in range(config["num_layers"])
|
||||
]
|
||||
)
|
||||
self._linear = nn.Linear(config["embed_dim"], config["vocab_size"])
|
||||
|
||||
|
||||
@property
|
||||
def max_seq_len(self):
|
||||
"""Возвращает максимальную длину последовательности."""
|
||||
return self._max_seq_len
|
||||
|
||||
def forward(self, x: torch.Tensor, attention_mask=None) -> torch.Tensor:
|
||||
"""Прямой проход через GPT
|
||||
|
||||
"""
|
||||
Прямой проход для получения логитов по последовательности токенов.
|
||||
|
||||
Args:
|
||||
x: Входной тензор [batch_size, seq_len]
|
||||
|
||||
-----
|
||||
x : torch.Tensor [batch, seq_len]
|
||||
Индексы входных токенов.
|
||||
use_cache : bool, optional
|
||||
Использовать ли кэш attention (ускоряет инференс, важно для генерации)
|
||||
cache : list, optional
|
||||
Список старых KV (key/value)-кэшей
|
||||
|
||||
Returns:
|
||||
Тензор логитов [batch_size, seq_len, vocab_size]
|
||||
--------
|
||||
logits: [batch, seq_len, vocab_size] (логиты для softmax по словарю)
|
||||
new_cache: кэш KV после прохода
|
||||
"""
|
||||
# Проверка длины последовательности
|
||||
if x.size(1) > self._max_seq_len:
|
||||
raise ValueError(f"Длина последовательности {x.size(1)} превышает максимальную {self._max_seq_len}")
|
||||
|
||||
raise ValueError(
|
||||
f"Длина последовательности {x.size(1)} превышает максимальную {self._max_seq_len}"
|
||||
)
|
||||
|
||||
# Эмбеддинги токенов и позиций
|
||||
tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]
|
||||
pos_out = self._position_embeddings(x.size(1)) # [seq_len, emb_size]
|
||||
|
||||
|
||||
# Комбинирование
|
||||
out = self._dropout(tok_out + pos_out.unsqueeze(0)) # [batch, seq_len, emb_size]
|
||||
|
||||
out = self._dropout(
|
||||
tok_out + pos_out.unsqueeze(0)
|
||||
) # [batch, seq_len, emb_size]
|
||||
|
||||
# Стек декодеров
|
||||
for decoder in self._decoders:
|
||||
out = decoder(out)
|
||||
|
||||
|
||||
return self._linear(out) # [batch, seq_len, vocab_size]
|
||||
|
||||
# def forward(self, input_ids, attention_mask=None):
|
||||
# B, T = input_ids.size()
|
||||
# pos = torch.arange(0, T, device=input_ids.device).unsqueeze(0)
|
||||
#
|
||||
# x = self.token_emb(input_ids) + self.pos_emb(pos)
|
||||
#
|
||||
# for block in self.blocks:
|
||||
# x = block(x, attention_mask)
|
||||
#
|
||||
# x = self.ln_f(x)
|
||||
# logits = self.head(x)
|
||||
# return logits
|
||||
|
||||
# def forward(self, input_ids, attention_mask=None):
|
||||
# B, T = input_ids.size()
|
||||
# pos = torch.arange(0, T, device=input_ids.device).unsqueeze(0)
|
||||
#
|
||||
# x = self.token_emb(input_ids) + self.pos_emb(pos)
|
||||
#
|
||||
# for block in self.blocks:
|
||||
# x = block(x, attention_mask)
|
||||
#
|
||||
# x = self.ln_f(x)
|
||||
# logits = self.head(x)
|
||||
# return logits
|
||||
|
||||
|
||||
def generate(self,
|
||||
x: torch.Tensor,
|
||||
max_new_tokens: int,
|
||||
def generate(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
max_new_tokens: int,
|
||||
do_sample: bool,
|
||||
temperature: float = 1.0,
|
||||
top_k: int = None,
|
||||
top_p: float = None,
|
||||
attention_mask: torch.Tensor = None, # Добавляем для совместимости с HF
|
||||
**kwargs # Игнорируем остальные параметры
|
||||
**kwargs, # Игнорируем остальные параметры
|
||||
) -> torch.Tensor:
|
||||
"""Авторегрессивная генерация текста.
|
||||
|
||||
Параметры:
|
||||
x: Входной тензор с индексами токенов формы [batch_size, seq_len],
|
||||
где batch_size - размер батча, seq_len - длина последовательности.
|
||||
max_new_tokens: Максимальное количество новых токенов для генерации.
|
||||
do_sample: Флаг выбора режима генерации:
|
||||
- True: вероятностное сэмплирование
|
||||
- False: жадный поиск (argmax)
|
||||
temperature: Параметр температуры для сэмплирования:
|
||||
- >1.0 - более случайные результаты
|
||||
- 1.0 - нейтральное значение
|
||||
- <1.0 - более предсказуемые результаты
|
||||
Должна быть > 0 (по умолчанию: 1.0)
|
||||
top_k: Если задан (и do_sample=True), используется top-k сэмплирование:
|
||||
- Выбираются только top_k самых вероятных токенов
|
||||
- Остальным токенам устанавливается вероятность 0
|
||||
- None: отключено (по умолчанию)
|
||||
top_p: Если задан (и do_sample=True), используется nucleus (top-p) сэмплирование:
|
||||
- Выбираются токены с кумулятивной вероятностью ≤ top_p
|
||||
- Гарантируется, что хотя бы один токен остаётся (даже если его вероятность > top_p)
|
||||
- None: отключено (по умолчанию)
|
||||
- Должен быть в диапазоне (0, 1]
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Тензор с расширенной последовательностью токенов формы
|
||||
[batch_size, seq_len + max_new_tokens]
|
||||
|
||||
Исключения:
|
||||
ValueError: Если входная последовательность длиннее max_seq_len
|
||||
ValueError: Если temperature <= 0
|
||||
ValueError: Если одновременно заданы top_k и top_p
|
||||
ValueError: Если top_k задан и ≤ 0
|
||||
ValueError: Если top_p задан и не в диапазоне (0, 1]
|
||||
|
||||
Примеры:
|
||||
>>> # Жадная генерация
|
||||
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=False)
|
||||
>>>
|
||||
>>> # Вероятностная генерация с top-k
|
||||
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, top_k=50)
|
||||
>>>
|
||||
>>> # Nucleus sampling (top-p)
|
||||
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, top_p=0.9)
|
||||
>>>
|
||||
>>> # Комбинация температуры и top-k
|
||||
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True,
|
||||
... temperature=0.7, top_k=50)
|
||||
|
||||
Примечания:
|
||||
1. Для детерминированных результатов в режиме сэмплирования
|
||||
зафиксируйте random seed (torch.manual_seed).
|
||||
2. Температура влияет только на режим сэмплирования (do_sample=True).
|
||||
3. Одновременное использование top_k и top_p запрещено.
|
||||
4. При do_sample=False параметры top_k, top_p и temperature игнорируются.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): Входной тензор с индексами токенов формы [batch_size, seq_len],
|
||||
где batch_size - размер батча, seq_len - длина последовательности.
|
||||
"""
|
||||
Авторегрессивная генерация текста с поддержкой жадного поиска (greedy), вероятностного сэмплирования с температурой,
|
||||
top-k и nucleus (top-p) sampling.
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор с индексами токенов, форма [batch_size, seq_len].
|
||||
max_new_tokens (int): Максимальное количество новых токенов для генерации.
|
||||
do_sample (bool): Флаг выбора режима генерации:
|
||||
- True: вероятностное сэмплирование
|
||||
- False: жадный поиск (argmax)
|
||||
temperature (float): Параметр температуры для сэмплирования:
|
||||
- >1.0 - более случайные результаты
|
||||
- 1.0 - нейтральное значение
|
||||
- <1.0 - более предсказуемые результаты
|
||||
Должна быть > 0 (по умолчанию: 1.0)
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Тензор с расширенной последовательностью токенов формы
|
||||
[batch_size, seq_len + max_new_tokens]
|
||||
|
||||
Raises:
|
||||
ValueError: Если входная последовательность длиннее max_seq_len
|
||||
ValueError: Если temperature <= 0
|
||||
|
||||
Examples:
|
||||
>>> # Жадная генерация
|
||||
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=False)
|
||||
>>>
|
||||
do_sample (bool): Если True — вероятностное сэмплирование; если False — жадная генерация (argmax).
|
||||
temperature (float): Температура для управления случайностью (>0, влияет только если do_sample=True).
|
||||
>1.0 — более случайно, <1.0 — более детерминированно.
|
||||
top_k (int, опц.): При do_sample=True ограничивает выбор top_k самых вероятных токенов (top-k sampling).
|
||||
top_p (float, опц.): При do_sample=True включает top-p (nucleus) sampling: кумулятивная вероятность ≤ top_p.
|
||||
Должно быть в (0, 1].
|
||||
attention_mask (torch.Tensor, опц.): Внешняя маска внимания (для совместимости с HuggingFace).
|
||||
**kwargs: Игнорируются.
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Последовательность токенов [batch_size, seq_len + max_new_tokens].
|
||||
|
||||
Исключения:
|
||||
ValueError: Если x длиннее max_seq_len модели.
|
||||
ValueError: Если temperature ≤ 0.
|
||||
ValueError: Если одновременно заданы top_k и top_p.
|
||||
ValueError: Если top_k ≤ 0.
|
||||
ValueError: Если top_p вне диапазона (0, 1].
|
||||
|
||||
Примеры:
|
||||
>>> # Жадная (детерминированная) генерация
|
||||
>>> output = model.generate(input_ids, max_new_tokens=12, do_sample=False)
|
||||
>>> # Вероятностная генерация с температурой
|
||||
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, temperature=0.7)
|
||||
>>>
|
||||
>>> # Более случайная генерация
|
||||
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, temperature=1.5)
|
||||
|
||||
Note:
|
||||
Для детерминированных результатов в режиме сэмплирования
|
||||
зафиксируйте random seed (torch.manual_seed).
|
||||
Температура влияет только на режим сэмплирования (do_sample=True).
|
||||
>>> output = model.generate(input_ids, max_new_tokens=12, do_sample=True, temperature=0.8)
|
||||
>>> # Top-k сэмплирование
|
||||
>>> output = model.generate(input_ids, max_new_tokens=12, do_sample=True, top_k=50)
|
||||
>>> # Top-p (nucleus) sampling
|
||||
>>> output = model.generate(input_ids, max_new_tokens=12, do_sample=True, top_p=0.92)
|
||||
>>> # Комбинация температуры и top-k
|
||||
>>> output = model.generate(input_ids, max_new_tokens=12, do_sample=True, temperature=1.0, top_k=100)
|
||||
|
||||
Примечания:
|
||||
- Для детерминированных выборок зафиксируйте random seed через torch.manual_seed.
|
||||
- Параметры temperature, top_k, top_p применимы только если do_sample=True.
|
||||
- Одновременное использование top_k и top_p не допускается.
|
||||
- Модель всегда возвращает тензор индексов токенов; для получения логитов используйте прямой вызов forward.
|
||||
|
||||
Ссылки:
|
||||
- Holtzman et al., "The Curious Case of Neural Text Degeneration" (nucleus sampling): https://arxiv.org/abs/1904.09751
|
||||
- Оригинальный GPT-2: https://cdn.openai.com/better-language-models/language-models.pdf
|
||||
"""
|
||||
for _ in range(max_new_tokens):
|
||||
# 1. Обрезаем вход, если последовательность слишком длинная
|
||||
x_cond = x[:, -self._max_seq_len:]
|
||||
x_cond = x[:, -self._max_seq_len :]
|
||||
|
||||
# 2. Передаем последовательность в метод forward класса GPT и полуаем логиты.
|
||||
logits = self.forward(x_cond)
|
||||
@@ -216,9 +268,14 @@ class GPT(BaseModel):
|
||||
vocab_size = logits_scaled.size(-1)
|
||||
|
||||
# создаём маску: True, если токен НЕ в topk_indices
|
||||
mask = torch.ones_like(logits_scaled, dtype=torch.bool if hasattr(torch, 'bool') else torch.uint8)
|
||||
mask.scatter_(1, topk_indices, False if hasattr(torch, 'bool') else 0) # False там, где top-k индексы
|
||||
masked_logits[mask] = float('-inf')
|
||||
mask = torch.ones_like(
|
||||
logits_scaled,
|
||||
dtype=torch.bool if hasattr(torch, "bool") else torch.uint8,
|
||||
)
|
||||
mask.scatter_(
|
||||
1, topk_indices, False if hasattr(torch, "bool") else 0
|
||||
) # False там, где top-k индексы
|
||||
masked_logits[mask] = float("-inf")
|
||||
|
||||
logits_scaled = masked_logits
|
||||
|
||||
@@ -226,36 +283,42 @@ class GPT(BaseModel):
|
||||
# 1. Применим softmax, чтобы получить вероятности:
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [B, vocab_size]
|
||||
# 2. Отсортируем токены по убыванию вероятностей:
|
||||
sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)
|
||||
sorted_probs, sorted_indices = torch.sort(
|
||||
probs, descending=True, dim=-1
|
||||
)
|
||||
# 3. Посчитаем кумулятивную сумму вероятностей:
|
||||
cum_probs = torch.cumsum(sorted_probs, dim=-1) # [B, vocab_size]
|
||||
# 4. Определим маску: оставить токены, пока сумма < top_p
|
||||
sorted_mask = (cum_probs <= top_p) # [B, vocab_size]
|
||||
sorted_mask = cum_probs <= top_p # [B, vocab_size]
|
||||
# Гарантируем, что хотя бы первый токен останется
|
||||
sorted_mask[:, 0] = True
|
||||
# 5. Преобразуем маску обратно в оригинальный порядок:
|
||||
# Создаём полную маску из False
|
||||
mask = torch.zeros_like(probs, dtype=torch.bool if hasattr(torch, 'bool') else torch.uint8)
|
||||
mask = torch.zeros_like(
|
||||
probs, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8
|
||||
)
|
||||
# Устанавливаем True в местах нужных токенов
|
||||
mask.scatter_(dim=1, index=sorted_indices, src=sorted_mask)
|
||||
# 6. Зануляем логиты токенов вне топ-p:
|
||||
logits_scaled[~mask] = float('-inf')
|
||||
logits_scaled[~mask] = float("-inf")
|
||||
|
||||
# 4. Применяем Softmax
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [batch_size, vocab_size]
|
||||
|
||||
|
||||
if do_sample == True:
|
||||
# 5. Если do_sample равен True, то отбираем токен случайно с помощью torch.multinomial
|
||||
next_token = torch.multinomial(probs, num_samples=1) # [batch_size, 1]
|
||||
else:
|
||||
# 5. Если do_sample равен False, то выбираем токен с максимальной вероятностью
|
||||
next_token = torch.argmax(probs, dim=-1, keepdim=True) # [batch_size, 1]
|
||||
|
||||
next_token = torch.argmax(
|
||||
probs, dim=-1, keepdim=True
|
||||
) # [batch_size, 1]
|
||||
|
||||
# 6. Добавляем его к последовательности
|
||||
x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1]
|
||||
return x
|
||||
|
||||
|
||||
# def generate(self, input_ids, max_length=50):
|
||||
# for _ in range(max_length):
|
||||
# logits = self.forward(input_ids)
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
"""
|
||||
GPT-2 — масштабируемый автогерессивный языковой трансформер второго поколения от OpenAI (2019).
|
||||
|
||||
Научная суть:
|
||||
- В сравнении с классическим GPT, layer normalization теперь применяется ПЕРЕД attention и FFN.
|
||||
- Позволило сильно увеличить глубину и размер модели (GPT2-модели имеют от 117M до 1.5B параметров).
|
||||
- Используется GELU активация; эффективное кэширование KV attention для генерации.
|
||||
|
||||
Формула attention-блока:
|
||||
LN(x) → Attention → рез. связь → LN → FFN → рез. связь
|
||||
|
||||
Подробнее:
|
||||
Radford et al. "Language Models are Unsupervised Multitask Learners"
|
||||
https://cdn.openai.com/better-language-models/language-models.pdf
|
||||
|
||||
Пример использования:
|
||||
>>> model = GPT2({"vocab_size": 50257, ...})
|
||||
>>> logits = model(input_ids)
|
||||
>>> out = model.generate(input_ids, max_length=30)
|
||||
"""
|
||||
import torch
|
||||
from torch import nn, Tensor
|
||||
import torch.nn.functional as F
|
||||
@@ -5,48 +25,146 @@ from llm.core.base_model import BaseModel
|
||||
from llm.core.token_embeddings import TokenEmbeddings
|
||||
from llm.core.positional_embeddings import PositionalEmbeddings
|
||||
from llm.core.cached_decoder import CachedDecoder
|
||||
from llm.core.feed_forward import FeedForward
|
||||
|
||||
|
||||
class GPT2(BaseModel):
|
||||
"""
|
||||
GPT-2 — масштабируемый автогерессивный языковой трансформер второго поколения от OpenAI (2019).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Позволяет предсказывать и порождать последовательности текста по одному токену, будучи обученным на задаче language modeling.
|
||||
- Модель реализует архитектуру decoder-only Transformer с Pre-LN (LayerNorm перед attention и FFN).
|
||||
- Используется для генерации, обучения с подкреплением для RLHF, zero/few-shot inference, чат-ботов и др.
|
||||
|
||||
Архитектурные особенности:
|
||||
--------------------------
|
||||
- Token и positional embeddings (learnable, как в GPT-2 оригинале).
|
||||
- Stack из N блоков Decoder (MultiHeadAttention с causal mask, Residual, Pre-LayerNorm, GELU FFN).
|
||||
- KV attention-кэш (ускоряет autoregressive generation, критически важно для LLM).
|
||||
- Использует GELU как функцию активации.
|
||||
- Поддержка dropout на каждом этапе.
|
||||
|
||||
Основные параметры:
|
||||
-------------------
|
||||
config: dict — параметры модели:
|
||||
vocab_size, # размер словаря токенов
|
||||
embed_dim, # размерность эмбеддинга
|
||||
num_heads, # количество attention голов
|
||||
num_layers, # глубина модели (число блоков)
|
||||
max_position_embeddings,
|
||||
dropout
|
||||
|
||||
Процессинг:
|
||||
-----------
|
||||
x (индексы токенов) → token_embeddings + position_embeddings → dropout
|
||||
→ stack Decoder blocks (masked attention, pre-LN)
|
||||
→ LayerNorm
|
||||
→ Linear(out_dim=vocab_size) → выходные логиты
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> gpt2 = GPT2({...})
|
||||
>>> logits = gpt2(input_ids)
|
||||
>>> output = gpt2.generate(input_ids, max_new_tokens=20, do_sample=True)
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Radford et al., "Language Models are Unsupervised Multitask Learners" (GPT-2, 2019): https://cdn.openai.com/better-language-models/language-models.pdf
|
||||
- HuggingFace GPT-2: https://github.com/huggingface/transformers/blob/main/src/transformers/models/gpt2/modeling_gpt2.py
|
||||
- Репликация в NanoGPT: https://github.com/karpathy/nanoGPT
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Инициализация GPT-2.
|
||||
|
||||
Args:
|
||||
config (dict): Параметры архитектуры:
|
||||
vocab_size: int — размер словаря
|
||||
embed_dim: int — размерность эмбеддинга
|
||||
num_heads: int — количество attention-голов
|
||||
num_layers: int — количество декодер-блоков
|
||||
max_position_embeddings: максимальная длина последовательности
|
||||
dropout: float — dropout
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Создаёт токеновые и позиционные эмбеддинги, стек декодеров, финальный LayerNorm и линейную проекцию в словарь.
|
||||
"""
|
||||
super().__init__(config)
|
||||
|
||||
# Инициализация слоев
|
||||
self._max_seq_len = config["max_position_embeddings"]
|
||||
self._token_embeddings = TokenEmbeddings(
|
||||
vocab_size=config["vocab_size"],
|
||||
emb_size=config["embed_dim"]
|
||||
vocab_size=config["vocab_size"], emb_size=config["embed_dim"]
|
||||
)
|
||||
self._position_embeddings = PositionalEmbeddings(
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
emb_size=config["embed_dim"]
|
||||
max_seq_len=config["max_position_embeddings"], emb_size=config["embed_dim"]
|
||||
)
|
||||
self._dropout = nn.Dropout(config["dropout"])
|
||||
# head_size = emb_size // num_heads
|
||||
self._decoders = nn.ModuleList([CachedDecoder(
|
||||
num_heads=config["num_heads"],
|
||||
emb_size=config["embed_dim"],
|
||||
head_size=config["embed_dim"] // config["num_heads"],
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
dropout=config["dropout"]
|
||||
) for _ in range(config["num_layers"])])
|
||||
self._decoders = nn.ModuleList(
|
||||
[
|
||||
CachedDecoder(
|
||||
num_heads=config["num_heads"],
|
||||
emb_size=config["embed_dim"],
|
||||
head_size=config["embed_dim"] // config["num_heads"],
|
||||
feed_forward_layer=FeedForward(
|
||||
emb_size=config["embed_dim"],
|
||||
dropout=config["dropout"],
|
||||
activation="gelu",
|
||||
),
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
dropout=config["dropout"],
|
||||
)
|
||||
for _ in range(config["num_layers"])
|
||||
]
|
||||
)
|
||||
self._norm = nn.LayerNorm(config["embed_dim"])
|
||||
self._linear = nn.Linear(config["embed_dim"], config["vocab_size"])
|
||||
|
||||
def forward(self, x: torch.Tensor, use_cache: bool = True, cache: list = None) -> tuple:
|
||||
def forward(
|
||||
self, x: torch.Tensor, use_cache: bool = True, cache: list = None
|
||||
) -> tuple:
|
||||
"""
|
||||
Прямой проход для batch of sequences (получение логитов по токенам).
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): Входной тензор с токенами [batch, seq_len]
|
||||
use_cache (bool): Использовать/возвращать кэш KV attention (ускоряет генерацию)
|
||||
cache (list / None): Внешний кэш KV attention (передаётся при генерации)
|
||||
|
||||
Returns:
|
||||
logits: torch.Tensor [batch, seq_len, vocab_size]
|
||||
new_cache: новый кэш KV attention (или None)
|
||||
|
||||
Пример:
|
||||
>>> logits, cache = gpt2(x, use_cache=True)
|
||||
"""
|
||||
# Проверка длины последовательности (только при отсутствии кэша)
|
||||
if cache is None and x.size(1) > self._max_seq_len:
|
||||
raise ValueError(f"Длина последовательности {x.size(1)} превышает максимальную {self.max_seq_len}")
|
||||
|
||||
|
||||
raise ValueError(
|
||||
f"Длина последовательности {x.size(1)} превышает максимальную {self.max_seq_len}"
|
||||
)
|
||||
|
||||
# Вычисление start_pos из кэша (если кэш передан)
|
||||
if cache is not None:
|
||||
# При кэше обрабатываем только один токен (последний)
|
||||
seq_len = 1
|
||||
# Вычисляем start_pos из самого нижнего уровня кэша
|
||||
if cache and cache[0] and cache[0][0]:
|
||||
key_cache, _ = cache[0][0] # Первый декодер, первая голова
|
||||
start_pos = key_cache.size(1) # cache_len
|
||||
# Безопасно извлекаем key_cache для вычисления start_pos
|
||||
if (
|
||||
isinstance(cache, (list, tuple))
|
||||
and len(cache) > 0
|
||||
and cache[0] is not None
|
||||
and isinstance(cache[0], (list, tuple))
|
||||
and len(cache[0]) > 0
|
||||
and cache[0][0] is not None
|
||||
and isinstance(cache[0][0], (tuple, list))
|
||||
and len(cache[0][0]) > 0
|
||||
):
|
||||
key_cache, _ = cache[0][0]
|
||||
start_pos = key_cache.size(1)
|
||||
else:
|
||||
start_pos = 0
|
||||
else:
|
||||
@@ -56,11 +174,15 @@ class GPT2(BaseModel):
|
||||
|
||||
# Эмбеддинги токенов и позиций
|
||||
tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]
|
||||
pos_out = self._position_embeddings(seq_len, start_pos=start_pos) # [seq_len, emb_size]
|
||||
|
||||
pos_out = self._position_embeddings(
|
||||
seq_len, start_pos=start_pos
|
||||
) # [seq_len, emb_size]
|
||||
|
||||
# Комбинирование
|
||||
out = self._dropout(tok_out + pos_out.unsqueeze(0)) # [batch, seq_len, emb_size]
|
||||
|
||||
out = self._dropout(
|
||||
tok_out + pos_out.unsqueeze(0)
|
||||
) # [batch, seq_len, emb_size]
|
||||
|
||||
# Стек декодеров с передачей кэша
|
||||
new_cache = []
|
||||
for i, decoder in enumerate(self._decoders):
|
||||
@@ -76,22 +198,75 @@ class GPT2(BaseModel):
|
||||
|
||||
out = self._norm(out)
|
||||
logits = self._linear(out)
|
||||
|
||||
|
||||
# Возвращаем результат с учетом use_cache
|
||||
if use_cache:
|
||||
return (logits, new_cache)
|
||||
else:
|
||||
return (logits, None)
|
||||
|
||||
def generate(self,
|
||||
x: torch.Tensor,
|
||||
max_new_tokens: int,
|
||||
def generate(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
max_new_tokens: int,
|
||||
do_sample: bool,
|
||||
temperature: float = 1.0,
|
||||
top_k: int = None,
|
||||
top_p: float = None,
|
||||
use_cache: bool = True
|
||||
use_cache: bool = True,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Авторегрессивная генерация токенов с поддержкой greedy, temperature, top-k, top-p sampling и KV-кэша.
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор с индексами токенов [batch_size, seq_len].
|
||||
max_new_tokens (int): Максимальное количество новых токенов для генерации.
|
||||
do_sample (bool): Режим генерации:
|
||||
- True: вероятностное сэмплирование (random sampling)
|
||||
- False: жадный (greedy) поиск (выбор argmax на каждом шаге)
|
||||
temperature (float): Температура распределения (>0, по умолчанию 1.0).
|
||||
- >1.0 — генерация более "творческая"/приподнятая вероятность "редких" токенов;
|
||||
- <1.0 — более предсказуемый и суженный выбор.
|
||||
top_k (int, опционально): Если задан, sampling только из top_k самых вероятных токенов (top-k sampling).
|
||||
top_p (float, опционально): Если задан, sampling только из токенов, кумулятивная вероятность которых ≤ top_p (nucleus/top-p sampling, см. Holtzman et al., 2019).
|
||||
use_cache (bool, по умолчанию True): Использовать кэш attention KV для ускорения авторегрессии.
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Тензор индексов токенов [batch_size, seq_len + max_new_tokens].
|
||||
|
||||
Исключения:
|
||||
ValueError: Если x длиннее максимальной длины (max_seq_len).
|
||||
ValueError: Если temperature ≤ 0.
|
||||
ValueError: Если одновременно заданы top_k и top_p.
|
||||
ValueError: Если top_k ≤ 0.
|
||||
ValueError: Если top_p не в диапазоне (0, 1].
|
||||
|
||||
Примеры использования:
|
||||
>>> # Жадная генерация
|
||||
>>> output = model.generate(input_ids, max_new_tokens=20, do_sample=False)
|
||||
|
||||
>>> # Сэмплирование с температурой
|
||||
>>> output = model.generate(input_ids, max_new_tokens=20, do_sample=True, temperature=0.8)
|
||||
|
||||
>>> # Top-k sampling
|
||||
>>> output = model.generate(input_ids, max_new_tokens=20, do_sample=True, top_k=50)
|
||||
|
||||
>>> # Top-p (nucleus) sampling
|
||||
>>> output = model.generate(input_ids, max_new_tokens=20, do_sample=True, top_p=0.92)
|
||||
|
||||
>>> # Комбинация температуры и top-k
|
||||
>>> output = model.generate(input_ids, max_new_tokens=20, do_sample=True, temperature=0.7, top_k=40)
|
||||
|
||||
Примечания:
|
||||
- Для детерминированных результатов используйте torch.manual_seed.
|
||||
- temperature, top_k, top_p работают только при do_sample=True.
|
||||
- Только один из top_k/top_p может быть задан одновременно.
|
||||
- Метод всегда возвращает индексы токенов (ids); для получения логитов используйте forward.
|
||||
|
||||
Ссылки:
|
||||
- Holtzman et al., "The Curious Case of Neural Text Degeneration" (nucleus sampling): https://arxiv.org/abs/1904.09751
|
||||
- Оригинальная статья GPT-2: https://cdn.openai.com/better-language-models/language-models.pdf
|
||||
"""
|
||||
cache = None
|
||||
|
||||
for _ in range(max_new_tokens):
|
||||
@@ -101,10 +276,10 @@ class GPT2(BaseModel):
|
||||
else:
|
||||
# Первая итерация или кэш отключен - передаем всю последовательность
|
||||
x_input = x
|
||||
|
||||
|
||||
# Прямой проход с кэшем
|
||||
logits, new_cache = self.forward(x_input, use_cache=use_cache, cache=cache)
|
||||
|
||||
|
||||
# Обновляем кэш для следующей итерации
|
||||
if use_cache:
|
||||
cache = new_cache
|
||||
@@ -125,26 +300,27 @@ class GPT2(BaseModel):
|
||||
vocab_size = logits_scaled.size(-1)
|
||||
|
||||
# создаём маску: 1, если токен НЕ в topk_indices
|
||||
mask = torch.ones_like(logits_scaled, dtype=torch.uint8)
|
||||
mask.scatter_(1, topk_indices, 0) # 0 там, где top-k индексы
|
||||
masked_logits[mask.byte()] = float('-inf')
|
||||
|
||||
mask = torch.ones_like(logits_scaled, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
mask.scatter_(1, topk_indices, False if hasattr(torch, "bool") else 0) # 0 там, где top-k индексы
|
||||
masked_logits[mask.bool() if hasattr(torch, "bool") else mask.byte()] = float('-inf')
|
||||
logits_scaled = masked_logits
|
||||
|
||||
if do_sample == True and top_p != None:
|
||||
# 1. Применим softmax, чтобы получить вероятности:
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [B, vocab_size]
|
||||
# 2. Отсортируем токены по убыванию вероятностей:
|
||||
sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)
|
||||
sorted_probs, sorted_indices = torch.sort(
|
||||
probs, descending=True, dim=-1
|
||||
)
|
||||
# 3. Посчитаем кумулятивную сумму вероятностей:
|
||||
cum_probs = torch.cumsum(sorted_probs, dim=-1) # [B, vocab_size]
|
||||
# 4. Определим маску: оставить токены, пока сумма < top_p
|
||||
sorted_mask = (cum_probs <= top_p).byte() # [B, vocab_size]
|
||||
sorted_mask = (cum_probs <= top_p).bool() if hasattr(torch, "bool") else (cum_probs <= top_p).byte() # [B, vocab_size]
|
||||
# Гарантируем, что хотя бы первый токен останется
|
||||
sorted_mask[:, 0] = 1
|
||||
sorted_mask[:, 0] = True if hasattr(torch, "bool") else 1
|
||||
# 5. Преобразуем маску обратно в оригинальный порядок:
|
||||
# Создаём полную маску из 0
|
||||
mask = torch.zeros_like(probs, dtype=torch.uint8)
|
||||
mask = torch.zeros_like(probs, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
# Устанавливаем 1 в местах нужных токенов
|
||||
mask.scatter_(dim=1, index=sorted_indices, src=sorted_mask)
|
||||
# 6. Зануляем логиты токенов вне топ-p:
|
||||
@@ -153,18 +329,19 @@ class GPT2(BaseModel):
|
||||
# 4. Применяем Softmax
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [batch_size, vocab_size]
|
||||
|
||||
|
||||
if do_sample == True:
|
||||
# 5. Если do_sample равен True, то отбираем токен случайно с помощью torch.multinomial
|
||||
next_token = torch.multinomial(probs, num_samples=1) # [batch_size, 1]
|
||||
else:
|
||||
# 5. Если do_sample равен False, то выбираем токен с максимальной вероятностью
|
||||
next_token = torch.argmax(probs, dim=-1, keepdim=True) # [batch_size, 1]
|
||||
|
||||
next_token = torch.argmax(
|
||||
probs, dim=-1, keepdim=True
|
||||
) # [batch_size, 1]
|
||||
|
||||
# 6. Добавляем его к последовательности
|
||||
x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1]
|
||||
return x
|
||||
|
||||
@property
|
||||
def max_seq_len(self) -> int:
|
||||
return self._max_seq_len
|
||||
return self._max_seq_len
|
||||
|
||||
3
llm/src/llm/models/llama/__init__.py
Normal file
3
llm/src/llm/models/llama/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .llama import Llama
|
||||
|
||||
__all__ = ["Llama"]
|
||||
303
llm/src/llm/models/llama/llama.py
Normal file
303
llm/src/llm/models/llama/llama.py
Normal file
@@ -0,0 +1,303 @@
|
||||
import torch
|
||||
from torch import nn, Tensor
|
||||
import torch.nn.functional as F
|
||||
|
||||
from llm.core.base_model import BaseModel
|
||||
from llm.core.token_embeddings import TokenEmbeddings
|
||||
from llm.core.swi_glu import SwiGLU
|
||||
from llm.core.rms_norm import RMSNorm
|
||||
from llm.core.rope import RoPE
|
||||
from llm.core.cached_decoder import CachedDecoder
|
||||
|
||||
|
||||
class Llama(BaseModel):
|
||||
"""
|
||||
LLaMA — автогерессивная большая языковая модель (Large Language Model from Meta, 2023).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Модель реализует архитектуру decoder-only Transformer с современными "индустриальными" трюками (RMSNorm, SwiGLU, RoPE, GQA).
|
||||
- Предназначена для генерации текста, чат-ботов, zero-/few-shot вывода, fine-tune в стиле RLHF, transfer learning и исследований в LLM.
|
||||
|
||||
Архитектурные особенности:
|
||||
--------------------------
|
||||
- Токеновые эмбеддинги и позиционное кодирование с помощью Rotary Position Embedding (RoPE, https://arxiv.org/abs/2104.09864).
|
||||
- Stack из num_layers современных декодеров с Grouped Query Attention (GQA: num_q_heads > num_kv_heads) для эффективной генерации.
|
||||
- FeedForward блоки с SwiGLU (см. https://arxiv.org/abs/2002.05202).
|
||||
- Нормализация RMSNorm перед каждым sub-layer (вот почему "Pre-RMSNorm").
|
||||
- Кэширование attention (KV cache) для быстрой autoregressive генерации.
|
||||
- Нет bias в Linear слоях, нет Dropout внутри attention.
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
config: dict с требуемыми ключами:
|
||||
vocab_size: int — размер словаря токенов
|
||||
embed_dim: int — размерность эмбеддингов
|
||||
num_q_heads: int — количество query-голов в attention (обычно больше num_kv_heads)
|
||||
num_kv_heads: int — количество key/value-голов
|
||||
num_layers: int — число слоёв-декодеров
|
||||
max_position_embeddings: int — максимальная длина последовательности
|
||||
window_size: int (optional) — размер sliding window для attention
|
||||
dropout: float (обычно 0.0 или очень мал)
|
||||
...
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> llama = LLaMA({...})
|
||||
>>> tokens = torch.tensor([[100, 56, 8]])
|
||||
>>> logits = llama(tokens)
|
||||
>>> out = llama.generate(tokens, max_new_tokens=10, do_sample=True, top_k=50)
|
||||
|
||||
References:
|
||||
-----------
|
||||
- "LLaMA: Open and Efficient Foundation Language Models" (Touvron et al., 2023): https://arxiv.org/abs/2302.13971
|
||||
- "Grouped-Query Attention": https://arxiv.org/abs/2307.09288
|
||||
- "RoFormer: Enhanced Transformer with Rotary Position Embedding": https://arxiv.org/abs/2104.09864
|
||||
- Discussion of efficient LLMs: https://huggingface.co/blog/mistral
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Инициализация LLaMA.
|
||||
|
||||
Args:
|
||||
config (dict): Параметры архитектуры, см. docstring класса.
|
||||
Внутри:
|
||||
-------
|
||||
- Создаёт Embedding-слой, Rotary Position Embeddings (RoPE), стек слоёв с GQA, RMSNorm, SwiGLU.
|
||||
- Финальный слой нормализации и проекции на vocabulary.
|
||||
"""
|
||||
super().__init__(config)
|
||||
|
||||
# Инициализация слоев
|
||||
self._max_seq_len = config["max_position_embeddings"]
|
||||
self._token_embeddings = TokenEmbeddings(
|
||||
vocab_size=config["vocab_size"], emb_size=config["embed_dim"]
|
||||
)
|
||||
self._position_embeddings = RoPE(
|
||||
head_size=config["embed_dim"] // config["num_heads"],
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
)
|
||||
|
||||
self._dropout = nn.Dropout(config["dropout"])
|
||||
self._decoders = nn.ModuleList(
|
||||
[
|
||||
CachedDecoder(
|
||||
norm_layer=RMSNorm,
|
||||
num_heads=config["num_heads"],
|
||||
emb_size=config["embed_dim"],
|
||||
head_size=config["embed_dim"] // config["num_heads"],
|
||||
feed_forward_layer=SwiGLU(
|
||||
emb_size=config["embed_dim"],
|
||||
dropout=config["dropout"],
|
||||
),
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
rope=self._position_embeddings,
|
||||
dropout=config["dropout"],
|
||||
)
|
||||
for _ in range(config["num_layers"])
|
||||
]
|
||||
)
|
||||
self._norm = RMSNorm(config["embed_dim"])
|
||||
self._linear = nn.Linear(config["embed_dim"], config["vocab_size"])
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, use_cache: bool = True, cache: list = None
|
||||
) -> tuple:
|
||||
"""
|
||||
Прямой проход: возвращает logits (и возможно обновлённый cache) по входным токенам.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): [batch, seq_len] — индексы токенов, shape [batch, seq_len]
|
||||
use_cache (bool): использовать механизм KV cache (ускоряет autoregressive generation)
|
||||
cache (list or None): предыдущий кэш, если нужен
|
||||
|
||||
Returns:
|
||||
logits: torch.Tensor [batch, seq_len, vocab_size]
|
||||
new_cache: новый кэш attention (или None)
|
||||
"""
|
||||
# Проверка длины последовательности (только при отсутствии кэша)
|
||||
if cache is None and x.size(1) > self._max_seq_len:
|
||||
raise ValueError(
|
||||
f"Длина последовательности {x.size(1)} превышает максимальную {self.max_seq_len}"
|
||||
)
|
||||
|
||||
# Вычисление start_pos из кэша (если кэш передан)
|
||||
# if cache is not None:
|
||||
# # При кэше обрабатываем только один токен (последний)
|
||||
# seq_len = 1
|
||||
# # Вычисляем start_pos из самого нижнего уровня кэша
|
||||
# if cache and cache[0] and cache[0][0]:
|
||||
# key_cache, _ = cache[0][0] # Первый декодер, первая голова
|
||||
# start_pos = key_cache.size(1) # cache_len
|
||||
# else:
|
||||
# start_pos = 0
|
||||
# else:
|
||||
# # Без кэша работаем как раньше
|
||||
# start_pos = 0
|
||||
# seq_len = x.size(1)
|
||||
|
||||
# Эмбеддинги токенов и позиций
|
||||
tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]
|
||||
# pos_out = self._position_embeddings(x) # [batch, seq_len, emb_size]
|
||||
|
||||
# Комбинирование
|
||||
out = self._dropout(tok_out) # [batch, seq_len, emb_size]
|
||||
|
||||
# Стек декодеров с передачей кэша
|
||||
new_cache = []
|
||||
for i, decoder in enumerate(self._decoders):
|
||||
decoder_cache = cache[i] if cache is not None else None
|
||||
decoder_result = decoder(out, use_cache=use_cache, cache=decoder_cache)
|
||||
|
||||
# Извлекаем результат из кортежа
|
||||
if use_cache:
|
||||
out, decoder_new_cache = decoder_result
|
||||
new_cache.append(decoder_new_cache)
|
||||
else:
|
||||
out = decoder_result[0]
|
||||
|
||||
out = self._norm(out)
|
||||
logits = self._linear(out)
|
||||
|
||||
# Возвращаем результат с учетом use_cache
|
||||
if use_cache:
|
||||
return (logits, new_cache)
|
||||
else:
|
||||
return (logits, None)
|
||||
|
||||
def generate(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
max_new_tokens: int,
|
||||
do_sample: bool,
|
||||
temperature: float = 1.0,
|
||||
top_k: int = None,
|
||||
top_p: float = None,
|
||||
use_cache: bool = True,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Авторегрессивная генерация последовательностей на основе LLaMA (greedy, temperature, top-k, top-p/nucleus, поддержка KV-кэша).
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор с токенами shape [batch_size, seq_len].
|
||||
max_new_tokens (int): Максимальное количество новых токенов для генерации.
|
||||
do_sample (bool): Использовать вероятностное сэмплирование (True) или жадный режим (False, argmax).
|
||||
temperature (float): Температура (сглаживание распределения вероятностей, >0; по умолчанию 1.0).
|
||||
>1.0 — менее предсказуемые, более разнообразные выборки.
|
||||
<1.0 — более строгие, консервативные выборки.
|
||||
top_k (int, опционально): Top-k сэмплирование (ограничение выбора k самыми вероятными токенами).
|
||||
top_p (float, опционально): Nucleus (top-p) sampling (срез по кумулятивной вероятности ≤ top_p, см. Holtzman et al., 2019).
|
||||
use_cache (bool, по умолчанию True): Использовать KV-кэш для ускорения генерации.
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Последовательность токенов shape [batch_size, seq_len + max_new_tokens].
|
||||
|
||||
Исключения:
|
||||
ValueError: Если x длиннее максимально допустимой длины (max_seq_len модели).
|
||||
ValueError: Если temperature ≤ 0.
|
||||
ValueError: Если одновременно заданы top_k и top_p.
|
||||
ValueError: Если top_k ≤ 0.
|
||||
ValueError: Если top_p не в диапазоне (0, 1].
|
||||
|
||||
Примеры:
|
||||
>>> # Строго жадная генерация
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=False)
|
||||
>>> # Вероятностная генерация с температурой
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, temperature=0.7)
|
||||
>>> # Top-k sampling
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, top_k=50)
|
||||
>>> # Top-p (nucleus)
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, top_p=0.92)
|
||||
>>> # Комбинация температуры и top-k
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, temperature=1.0, top_k=100)
|
||||
|
||||
Примечания:
|
||||
- temperature, top_k, top_p применяются только если do_sample=True.
|
||||
- Одновременное использование top_k и top_p запрещено.
|
||||
- Для воспроизводимых результатов зафиксируйте seed через torch.manual_seed.
|
||||
- Возвращается только индексы токенов; для получения вероятностей используйте forward.
|
||||
|
||||
Ссылки:
|
||||
- Holtzman et al., "The Curious Case of Neural Text Degeneration" (nucleus/top-p): https://arxiv.org/abs/1904.09751
|
||||
- LLaMA: https://arxiv.org/abs/2302.13971
|
||||
"""
|
||||
cache = None
|
||||
|
||||
for _ in range(max_new_tokens):
|
||||
if use_cache and cache is not None:
|
||||
# Используем кэш - передаем только последний токен
|
||||
x_input = x[:, -1:] # [batch_size, 1]
|
||||
else:
|
||||
# Первая итерация или кэш отключен - передаем всю последовательность
|
||||
x_input = x
|
||||
|
||||
# Прямой проход с кэшем
|
||||
logits, new_cache = self.forward(x_input, use_cache=use_cache, cache=cache)
|
||||
|
||||
# Обновляем кэш для следующей итерации
|
||||
if use_cache:
|
||||
cache = new_cache
|
||||
|
||||
last_logits = logits[:, -1, :] # [batch_size, vocab_size]
|
||||
|
||||
# Масштабируем логиты температурой
|
||||
if temperature > 0:
|
||||
logits_scaled = last_logits / temperature
|
||||
else:
|
||||
logits_scaled = last_logits
|
||||
|
||||
if do_sample == True and top_k != None:
|
||||
_, topk_indices = torch.topk(logits_scaled, top_k, dim=-1)
|
||||
|
||||
# # Заменим все НЕ top-k логиты на -inf
|
||||
masked_logits = logits_scaled.clone()
|
||||
vocab_size = logits_scaled.size(-1)
|
||||
|
||||
# создаём маску: 1, если токен НЕ в topk_indices
|
||||
mask = torch.ones_like(logits_scaled, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
mask.scatter_(1, topk_indices, False if hasattr(torch, "bool") else 0) # 0 там, где top-k индексы
|
||||
masked_logits[mask.bool() if hasattr(torch, "bool") else mask.byte()] = float('-inf')
|
||||
logits_scaled = masked_logits
|
||||
|
||||
if do_sample == True and top_p != None:
|
||||
# 1. Применим softmax, чтобы получить вероятности:
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [B, vocab_size]
|
||||
# 2. Отсортируем токены по убыванию вероятностей:
|
||||
sorted_probs, sorted_indices = torch.sort(
|
||||
probs, descending=True, dim=-1
|
||||
)
|
||||
# 3. Посчитаем кумулятивную сумму вероятностей:
|
||||
cum_probs = torch.cumsum(sorted_probs, dim=-1) # [B, vocab_size]
|
||||
# 4. Определим маску: оставить токены, пока сумма < top_p
|
||||
sorted_mask = (cum_probs <= top_p).bool() if hasattr(torch, "bool") else (cum_probs <= top_p).byte() # [B, vocab_size]
|
||||
# Гарантируем, что хотя бы первый токен останется
|
||||
sorted_mask[:, 0] = True if hasattr(torch, "bool") else 1
|
||||
# 5. Преобразуем маску обратно в оригинальный порядок:
|
||||
# Создаём полную маску из 0
|
||||
mask = torch.zeros_like(probs, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
# Устанавливаем 1 в местах нужных токенов
|
||||
mask.scatter_(dim=1, index=sorted_indices, src=sorted_mask)
|
||||
# 6. Зануляем логиты токенов вне топ-p:
|
||||
logits_scaled[~mask] = float('-inf')
|
||||
|
||||
# 4. Применяем Softmax
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [batch_size, vocab_size]
|
||||
|
||||
if do_sample == True:
|
||||
# 5. Если do_sample равен True, то отбираем токен случайно с помощью torch.multinomial
|
||||
next_token = torch.multinomial(probs, num_samples=1) # [batch_size, 1]
|
||||
else:
|
||||
# 5. Если do_sample равен False, то выбираем токен с максимальной вероятностью
|
||||
next_token = torch.argmax(
|
||||
probs, dim=-1, keepdim=True
|
||||
) # [batch_size, 1]
|
||||
|
||||
# 6. Добавляем его к последовательности
|
||||
x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1]
|
||||
return x
|
||||
|
||||
@property
|
||||
def max_seq_len(self) -> int:
|
||||
return self._max_seq_len
|
||||
3
llm/src/llm/models/mistral/__init__.py
Normal file
3
llm/src/llm/models/mistral/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .mistral import Mistral
|
||||
|
||||
__all__ = ["Mistral"]
|
||||
273
llm/src/llm/models/mistral/mistral.py
Normal file
273
llm/src/llm/models/mistral/mistral.py
Normal file
@@ -0,0 +1,273 @@
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch import Tensor
|
||||
import torch.nn.functional as F
|
||||
from math import sqrt
|
||||
from llm.core.base_model import BaseModel
|
||||
from llm.core.token_embeddings import TokenEmbeddings
|
||||
from llm.core.rms_norm import RMSNorm
|
||||
from llm.core.rope import RoPE
|
||||
from llm.core.mistral_decoder import MistralDecoder
|
||||
|
||||
|
||||
class Mistral(BaseModel):
|
||||
"""
|
||||
Mistral — автогерессивная языковая LLM-архитектура (2023, Mistral AI) для быстрого и качественного моделирования текста.
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Модель построена на базе decoder-only Transformer с важными оптимизациями: GQA (Grouped Query Attention), RoPE, SwiGLU, RMSNorm, sliding window attention.
|
||||
- Поддерживает autoregressive generation (step-by-step текст), обучение и inference на длинных последовательностях.
|
||||
- Используется в современных open-source LLM: Mistral-7B, Mixtral-8x7B и др.
|
||||
|
||||
Архитектурные особенности:
|
||||
--------------------------
|
||||
- Токеновые эмбеддинги (TokenEmbeddings) и позиционное кодирование через RoPE (rotary position embedding).
|
||||
- Stack из num_layers декодеров с Grouped Query Attention (раздельное число query/key heads для оптимизации памяти).
|
||||
- Sliding Window Attention Mask — позволяет ускорять обработку длинных текстов, ограничивая область внимания для каждого токена (как в оригинальном Mistral).
|
||||
- SwiGLU FeedForward-блоки и RMSNorm.
|
||||
- Dropout (регуляризация).
|
||||
- Кэширование attention (KV cache) для быстрой генерации токенов по одному.
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
config (dict): параметры модели (см. документацию Mistral):
|
||||
vocab_size: int — размер словаря токенов
|
||||
embed_dim: int — размерность эмбеддингов
|
||||
num_q_heads: int — количество query-голов (обычно больше num_kv_heads)
|
||||
num_kv_heads: int — количество key/value attention-голов
|
||||
num_layers: int — число слоёв-декодеров
|
||||
max_position_embeddings: int — максимальная длина последовательности
|
||||
window_size: int — размер sliding window attention
|
||||
dropout: float — dropout (обычно очень мал или 0)
|
||||
...
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> model = Mistral({...})
|
||||
>>> tokens = torch.tensor([[100, 56, 8]])
|
||||
>>> logits = model(tokens)
|
||||
>>> generated = model.generate(tokens, max_new_tokens=16, do_sample=True, top_k=50)
|
||||
|
||||
References:
|
||||
-----------
|
||||
- "Mistral: Fast and Efficient Dense and Mixture of Experts Transformer Models" (2023): https://arxiv.org/abs/2310.06825
|
||||
- LLaMA v2 & Grouped-Query Attention: https://arxiv.org/abs/2307.09288
|
||||
- Оригинальное обсуждение архитектуры: https://huggingface.co/blog/mistral
|
||||
|
||||
"""
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
|
||||
self._max_seq_len = config["max_position_embeddings"]
|
||||
# Инициализация слоев
|
||||
self._token_embeddings = TokenEmbeddings(
|
||||
vocab_size=config["vocab_size"],
|
||||
emb_size=config["embed_dim"]
|
||||
)
|
||||
self._position_embeddings = RoPE(
|
||||
head_size=config["embed_dim"] // config["num_q_heads"],
|
||||
max_seq_len=config["max_position_embeddings"]
|
||||
)
|
||||
#self._position_embeddings = PositionalEmbeddings(
|
||||
# max_seq_len=max_seq_len,
|
||||
# emb_size=emb_size
|
||||
#)
|
||||
self._dropout = nn.Dropout(config["dropout"])
|
||||
self._decoders = nn.ModuleList([MistralDecoder(
|
||||
num_q_heads=config["num_q_heads"],
|
||||
num_kv_heads=config["num_kv_heads"],
|
||||
emb_size=config["embed_dim"],
|
||||
head_size=config["embed_dim"] // config["num_q_heads"],
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
window_size=config["window_size"],
|
||||
rope=self._position_embeddings,
|
||||
dropout=config["dropout"]
|
||||
) for _ in range(config["num_layers"])])
|
||||
self._norm = RMSNorm(config["embed_dim"])
|
||||
self._linear = nn.Linear(config["embed_dim"], config["vocab_size"])
|
||||
|
||||
def forward(self, x: torch.Tensor, use_cache: bool = True, cache: list = None) -> tuple:
|
||||
"""
|
||||
Прямой проход (forward) через всю модель Mistral: возвращает логиты для токенов и (опционально) кэш attention для ускорения autoregressive генерации.
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор с токенами (shape [batch_size, seq_len]), где значения — индексы токенов.
|
||||
use_cache (bool, по умолчанию True): Возвращать ли новый KV attention-кэш для последующей генерации.
|
||||
cache (list or None): Предыдущий кэш attention (или None для полного прохода без накопления кэша).
|
||||
|
||||
Возвращает:
|
||||
logits (torch.Tensor): Тензор логитов shape [batch_size, seq_len, vocab_size] — вероятностное распределение по словарю для каждого токена.
|
||||
new_cache (list or None): Новый кэш KV attention-слоев (или None, если use_cache=False).
|
||||
|
||||
Исключения:
|
||||
ValueError: Если длина последовательности превышает максимальную (max_seq_len), когда не используется кэш.
|
||||
|
||||
Пример:
|
||||
>>> logits, cache = model.forward(input_ids, use_cache=True)
|
||||
>>> probabilities = torch.softmax(logits, dim=-1)
|
||||
"""
|
||||
# Проверка длины последовательности (только при отсутствии кэша)
|
||||
if cache is None and x.size(1) > self._max_seq_len:
|
||||
raise ValueError(f"Длина последовательности {x.size(1)} превышает максимальную {self.max_seq_len}")
|
||||
|
||||
# Эмбеддинги токенов и позиций
|
||||
tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]
|
||||
#pos_out = self._position_embeddings(x) # [batch, seq_len, emb_size]
|
||||
|
||||
# Комбинирование
|
||||
out = self._dropout(tok_out) # [batch, seq_len, emb_size]
|
||||
|
||||
# Стек декодеров с передачей кэша
|
||||
new_cache = []
|
||||
for i, decoder in enumerate(self._decoders):
|
||||
decoder_cache = cache[i] if cache is not None else None
|
||||
decoder_result = decoder(out, use_cache=use_cache, cache=decoder_cache)
|
||||
|
||||
# Извлекаем результат из кортежа
|
||||
if use_cache:
|
||||
out, decoder_new_cache = decoder_result
|
||||
new_cache.append(decoder_new_cache)
|
||||
else:
|
||||
out = decoder_result[0]
|
||||
|
||||
out = self._norm(out)
|
||||
logits = self._linear(out)
|
||||
|
||||
# Возвращаем результат с учетом use_cache
|
||||
if use_cache:
|
||||
return (logits, new_cache)
|
||||
else:
|
||||
return (logits, None)
|
||||
|
||||
def generate(self,
|
||||
x: torch.Tensor,
|
||||
max_new_tokens: int,
|
||||
do_sample: bool,
|
||||
temperature: float = 1.0,
|
||||
top_k: int = None,
|
||||
top_p: float = None,
|
||||
use_cache: bool = True
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Авторегрессивная генерация токенов с поддержкой greedy, temperature, top-k/top-p sampling
|
||||
и ускорением через attention-кэш (KV-cache, важно для inference на длинных текстах).
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор с токенами shape [batch_size, seq_len].
|
||||
max_new_tokens (int): Максимальное количество новых токенов для генерации.
|
||||
do_sample (bool): Если True — вероятность/случайность (random sampling); если False — жадная генерация (argmax).
|
||||
temperature (float): Температура (>0, по умолчанию 1.0); >1.0 — более случайные выборы, <1.0 — более строгие.
|
||||
top_k (int, optional): top-k sampling; при сэмплировании выбираются только top_k наиболее вероятных токенов.
|
||||
top_p (float, optional): nucleus (top-p) sampling; выбираются токены с накопленной вероятностью ≤ top_p.
|
||||
use_cache (bool, по умолчанию True): Использовать ускорение через KV attention cache для autoregressive режима.
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Последовательность индексов токенов shape [batch_size, seq_len + max_new_tokens].
|
||||
|
||||
Исключения:
|
||||
ValueError: Если x длиннее max_seq_len модели.
|
||||
ValueError: Если temperature ≤ 0.
|
||||
ValueError: Если одновременно заданы top_k и top_p.
|
||||
ValueError: Если top_k ≤ 0.
|
||||
ValueError: Если top_p не в диапазоне (0, 1].
|
||||
|
||||
Примеры:
|
||||
>>> # Жадная генерация
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=False)
|
||||
>>> # Сэмплирование с температурой
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, temperature=0.8)
|
||||
>>> # Top-k sampling
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, top_k=50)
|
||||
>>> # Top-p (nucleus) sampling
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, top_p=0.92)
|
||||
>>> # Температура + top-k
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, temperature=1.0, top_k=100)
|
||||
|
||||
Примечания:
|
||||
- Одновременно использовать top_k и top_p нельзя.
|
||||
- Параметры temperature, top_k, top_p работают только при do_sample=True.
|
||||
- Для полного воспроизведения результата зафиксируйте seed через torch.manual_seed.
|
||||
- Метод всегда возвращает только индексы токенов; для получения логитов используйте forward.
|
||||
|
||||
Ссылки:
|
||||
- Holtzman et al., "The Curious Case of Neural Text Degeneration" (nucleus/top-p sampling): https://arxiv.org/abs/1904.09751
|
||||
- Mistral: https://arxiv.org/abs/2310.06825
|
||||
"""
|
||||
cache = None
|
||||
|
||||
for _ in range(max_new_tokens):
|
||||
if use_cache and cache is not None:
|
||||
# Используем кэш - передаем только последний токен
|
||||
x_input = x[:, -1:] # [batch_size, 1]
|
||||
else:
|
||||
# Первая итерация или кэш отключен - передаем всю последовательность
|
||||
x_input = x
|
||||
|
||||
# Прямой проход с кэшем
|
||||
logits, new_cache = self.forward(x_input, use_cache=use_cache, cache=cache)
|
||||
|
||||
# Обновляем кэш для следующей итерации
|
||||
if use_cache:
|
||||
cache = new_cache
|
||||
|
||||
last_logits = logits[:, -1, :] # [batch_size, vocab_size]
|
||||
|
||||
# Масштабируем логиты температурой
|
||||
if temperature > 0:
|
||||
logits_scaled = last_logits / temperature
|
||||
else:
|
||||
logits_scaled = last_logits
|
||||
|
||||
if do_sample == True and top_k != None:
|
||||
_, topk_indices = torch.topk(logits_scaled, top_k, dim=-1)
|
||||
|
||||
# # Заменим все НЕ top-k логиты на -inf
|
||||
masked_logits = logits_scaled.clone()
|
||||
vocab_size = logits_scaled.size(-1)
|
||||
|
||||
# создаём маску: 1, если токен НЕ в topk_indices
|
||||
mask = torch.ones_like(logits_scaled, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
mask.scatter_(1, topk_indices, False if hasattr(torch, "bool") else 0) # 0 там, где top-k индексы
|
||||
masked_logits[mask.bool() if hasattr(torch, "bool") else mask.byte()] = float('-inf')
|
||||
|
||||
logits_scaled = masked_logits
|
||||
|
||||
if do_sample == True and top_p != None:
|
||||
# 1. Применим softmax, чтобы получить вероятности:
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [B, vocab_size]
|
||||
# 2. Отсортируем токены по убыванию вероятностей:
|
||||
sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)
|
||||
# 3. Посчитаем кумулятивную сумму вероятностей:
|
||||
cum_probs = torch.cumsum(sorted_probs, dim=-1) # [B, vocab_size]
|
||||
# 4. Определим маску: оставить токены, пока сумма < top_p
|
||||
sorted_mask = (cum_probs <= top_p).bool() if hasattr(torch, "bool") else (cum_probs <= top_p).byte() # [B, vocab_size]
|
||||
# Гарантируем, что хотя бы первый токен останется
|
||||
sorted_mask[:, 0] = True if hasattr(torch, "bool") else 1
|
||||
# 5. Преобразуем маску обратно в оригинальный порядок:
|
||||
# Создаём полную маску из 0
|
||||
mask = torch.zeros_like(probs, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
# Устанавливаем 1 в местах нужных токенов
|
||||
mask.scatter_(dim=1, index=sorted_indices, src=sorted_mask)
|
||||
# 6. Зануляем логиты токенов вне топ-p:
|
||||
logits_scaled[~mask] = float('-inf')
|
||||
|
||||
# 4. Применяем Softmax
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [batch_size, vocab_size]
|
||||
|
||||
|
||||
if do_sample == True:
|
||||
# 5. Если do_sample равен True, то отбираем токен случайно с помощью torch.multinomial
|
||||
next_token = torch.multinomial(probs, num_samples=1) # [batch_size, 1]
|
||||
else:
|
||||
# 5. Если do_sample равен False, то выбираем токен с максимальной вероятностью
|
||||
next_token = torch.argmax(probs, dim=-1, keepdim=True) # [batch_size, 1]
|
||||
|
||||
# 6. Добавляем его к последовательности
|
||||
x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1]
|
||||
return x
|
||||
|
||||
|
||||
@property
|
||||
def max_seq_len(self) -> int:
|
||||
return self._max_seq_len
|
||||
3
llm/src/llm/models/mixtral/__init__.py
Normal file
3
llm/src/llm/models/mixtral/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .mixtral import Mixtral
|
||||
|
||||
__all__ = ["Mixtral"]
|
||||
358
llm/src/llm/models/mixtral/mixtral.py
Normal file
358
llm/src/llm/models/mixtral/mixtral.py
Normal file
@@ -0,0 +1,358 @@
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch import Tensor
|
||||
import torch.nn.functional as F
|
||||
from math import sqrt
|
||||
from llm.core.base_model import BaseModel
|
||||
from llm.core.token_embeddings import TokenEmbeddings
|
||||
from llm.core.rope import RoPE
|
||||
from llm.core.rms_norm import RMSNorm
|
||||
from llm.core.mixtral_decoder import MixtralDecoder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Mixtral(BaseModel):
|
||||
"""
|
||||
Mixtral — языковая модель с архитектурой Mixture-of-Experts на основе современных трансформеров (см. Mixtral 8x7B).
|
||||
|
||||
Описание:
|
||||
---------
|
||||
Данный класс реализует полностью функциональную LLM с блоками MixtralDecoder, которые используют разреженные Feed-Forward сети MoE (Mixture-of-Experts)
|
||||
и Grouped Query Attention (GQA). Позволяет масштабировать количество параметров без экспоненциального роста вычислительных затрат благодаря активации лишь части экспертов на каждый токен.
|
||||
Mixtral поддерживает автотекстогенерацию с caching, position encoding через RoPE и всё необходимое для работы и тренировки современных LLM.
|
||||
|
||||
Архитектурные особенности:
|
||||
--------------------------
|
||||
- Stack из N слоёв MixtralDecoder (каждый — MoE-блок + attention + RMSNorm).
|
||||
- Dropout для регуляризации на уровне эмбеддингов и слоёв.
|
||||
- Позиционные эмбеддинги реализованы через RoPE (Rotary Positional Embeddings).
|
||||
- Финальная RMSNorm плюс Linear-проекция к словарю токенов.
|
||||
- Поддержка автогенерации с sampling (greedy, top-k, top-p), temperature и KV-cache.
|
||||
|
||||
Аргументы конструктора:
|
||||
----------------------
|
||||
config : dict
|
||||
Словарь-конфиг с основными гиперпараметрами модели:
|
||||
- vocab_size : int — размер словаря токенов
|
||||
- embed_dim : int — размер скрытого пространства
|
||||
- max_position_embeddings : int — макс. длина последовательности
|
||||
- num_layers : int — количество декодерных блоков в стеке
|
||||
- num_q_heads : int — число query-голов в attention
|
||||
- num_kv_heads : int — число kv-голов в attention
|
||||
- num_experts : int — число MoE-экспертов
|
||||
- top_k_experts : int — сколько экспертов активировать на токен
|
||||
- dropout : float — вероятность Dropout
|
||||
- window_size : int — размер окна внимания
|
||||
|
||||
Основные методы:
|
||||
----------------
|
||||
- forward(x, use_cache=True, cache=None) — прямой проход, поддерживает batched вход, caching.
|
||||
- generate(...) — авторегрессивная генерация с разными стратегиями sampling и ускорением через cache.
|
||||
- save(path)/load(path, device) — сохранение и восстановление обученной модели.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> config = {...} # dict с параметрами
|
||||
>>> model = Mixtral(config)
|
||||
>>> x = torch.randint(0, config["vocab_size"], (2, 16))
|
||||
>>> logits, cache = model(x, use_cache=True)
|
||||
>>> print(logits.shape) # [2, 16, vocab_size]
|
||||
|
||||
>>> # Генерация
|
||||
>>> out = model.generate(x, max_new_tokens=20, do_sample=True, top_k=10, temperature=0.9)
|
||||
|
||||
Литература:
|
||||
-----------
|
||||
- Mixtral 8x7B: https://mistral.ai/news/mixtral-of-experts/
|
||||
- Switch Transformer: https://arxiv.org/abs/2101.03961
|
||||
- GShard: https://arxiv.org/abs/2006.16668
|
||||
- RoPE: https://arxiv.org/abs/2104.09864
|
||||
- Grouped Query Attention: https://arxiv.org/abs/2305.14236
|
||||
- RMSNorm: https://arxiv.org/abs/1910.07467
|
||||
"""
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Конструктор класса Mixtral.
|
||||
|
||||
Осуществляет инициализацию всех модулей и внутренних параметров большой языковой модели с архитектурой Mixtral/MoE.
|
||||
Использует параметры из конфиг-словаря `config` для гибкой настройки модели.
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
config : dict
|
||||
Словарь с основными гиперпараметрами архитектуры. Должен содержать ключи:
|
||||
vocab_size (int): Размер словаря токенов.
|
||||
embed_dim (int): Размер скрытого пространства (эмбеддингов).
|
||||
max_position_embeddings (int): Максимальная длина токенной последовательности.
|
||||
num_layers (int): Количество декодерных блоков (слоёв) в модели.
|
||||
num_q_heads (int): Число query-голов (attention heads).
|
||||
num_kv_heads (int): Число key-value голов (attention heads).
|
||||
num_experts (int): Количество экспертов в каждом MoE-блоке.
|
||||
top_k_experts (int): Сколько экспертов активируется для одного токена.
|
||||
dropout (float): Dropout для регуляризации.
|
||||
window_size (int): Размер окна внимания (Attention Window).
|
||||
|
||||
Внутри:
|
||||
-------
|
||||
- Инициализируются эмбеддинги токенов, позиционные эмбеддинги RoPE, Dropout.
|
||||
- Строится стек из num_layers модулей MixtralDecoder с заданным количеством attention heads и экспертов.
|
||||
- Финальный слой нормализации и проекция к логитам словаря (linear layer).
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> config = {
|
||||
... "vocab_size": 32000,
|
||||
... "embed_dim": 512,
|
||||
... "max_position_embeddings": 2048,
|
||||
... "num_layers": 24,
|
||||
... "num_q_heads": 8,
|
||||
... "num_kv_heads": 8,
|
||||
... "num_experts": 8,
|
||||
... "top_k_experts": 2,
|
||||
... "dropout": 0.1,
|
||||
... "window_size": 256,
|
||||
... }
|
||||
>>> model = Mixtral(config)
|
||||
|
||||
Примечания:
|
||||
-----------
|
||||
- Конфиг модели должен быть согласован: размеры должны делиться на число голов, число экспертов и top_k_experts корректно выбраны.
|
||||
- Все параметры, необходимые для построения MixtralDecoder, attention и MoE, берутся из config.
|
||||
"""
|
||||
super().__init__(config)
|
||||
|
||||
self._max_seq_len = config["max_position_embeddings"]
|
||||
|
||||
# Инициализация слоев
|
||||
self._token_embeddings = TokenEmbeddings(
|
||||
vocab_size=config["vocab_size"],
|
||||
emb_size=config["embed_dim"]
|
||||
)
|
||||
self._position_embeddings = RoPE(
|
||||
head_size=config["embed_dim"] // config["num_q_heads"],
|
||||
max_seq_len=config["max_position_embeddings"]
|
||||
)
|
||||
#self._position_embeddings = PositionalEmbeddings(
|
||||
# max_seq_len=max_seq_len,
|
||||
# emb_size=emb_size
|
||||
#)
|
||||
self._dropout = nn.Dropout(config["dropout"])
|
||||
self._decoders = nn.ModuleList([MixtralDecoder(
|
||||
num_q_heads=config["num_q_heads"],
|
||||
num_kv_heads=config["num_kv_heads"],
|
||||
emb_size=config["embed_dim"],
|
||||
head_size=config["embed_dim"] // config["num_q_heads"],
|
||||
max_seq_len=config["max_position_embeddings"],
|
||||
num_experts=config["num_experts"],
|
||||
top_k_experts=config["top_k_experts"],
|
||||
window_size=config["window_size"],
|
||||
rope=self._position_embeddings,
|
||||
dropout=config["dropout"]
|
||||
) for _ in range(config["num_layers"])])
|
||||
self._norm = RMSNorm(config["embed_dim"])
|
||||
self._linear = nn.Linear(config["embed_dim"], config["vocab_size"])
|
||||
|
||||
def forward(self, x: torch.Tensor, use_cache: bool = True, cache: list = None) -> tuple:
|
||||
"""
|
||||
Прямой проход (forward) через всю модель Mixtral.
|
||||
|
||||
Данный метод реализует трансформацию входной последовательности токенов в логиты (предсказания вероятностей токенов словаря)
|
||||
с поддержкой эффективного инференса с использованием cache (KV-кэш attention для автогенерации).
|
||||
|
||||
Аргументы:
|
||||
----------
|
||||
x : torch.Tensor
|
||||
Двумерный входной тензор shape [batch_size, seq_len], где каждое значение — ID токена.
|
||||
use_cache : bool, по умолчанию True
|
||||
Если True — в режиме генерации модель возвращает обновлённый список кэшей attention для ускорения последовательного инференса.
|
||||
Если False — attention cache не используется.
|
||||
cache : list, optional
|
||||
(Необязательно) Список (или None) с кэшем KV attention для каждого слоя. Используется для автогенерации текста.
|
||||
|
||||
Возвращает:
|
||||
-----------
|
||||
tuple:
|
||||
- logits : torch.Tensor — выходной тензор shape [batch_size, seq_len, vocab_size] — массив логитов по токенам и словарю.
|
||||
- new_cache : list или None — обновлённый cache, если используется.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> logits, new_cache = model(x, use_cache=True, cache=None)
|
||||
>>> logits.shape # [batch_size, seq_len, vocab_size]
|
||||
|
||||
Примечания:
|
||||
-----------
|
||||
- Если используется cache — эффективно для авторегрессионной генерации (token-by-token), например, при диалогах или длинной генерации.
|
||||
- Если входная последовательность длиннее max_seq_len — будет выброшено исключение.
|
||||
- Если нужен только логит последнего токена — используйте slice: logits[:, -1, :]
|
||||
|
||||
"""
|
||||
# Проверка длины последовательности (только при отсутствии кэша)
|
||||
if cache is None and x.size(1) > self._max_seq_len:
|
||||
raise ValueError(f"Длина последовательности {x.size(1)} превышает максимальную {self.max_seq_len}")
|
||||
|
||||
# Эмбеддинги токенов и позиций
|
||||
tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]
|
||||
#pos_out = self._position_embeddings(x) # [batch, seq_len, emb_size]
|
||||
|
||||
# Комбинирование
|
||||
out = self._dropout(tok_out) # [batch, seq_len, emb_size]
|
||||
|
||||
# Стек декодеров с передачей кэша
|
||||
new_cache = []
|
||||
for i, decoder in enumerate(self._decoders):
|
||||
decoder_cache = cache[i] if cache is not None else None
|
||||
decoder_result = decoder(out, use_cache=use_cache, cache=decoder_cache)
|
||||
|
||||
# Извлекаем результат из кортежа
|
||||
if use_cache:
|
||||
out, decoder_new_cache = decoder_result
|
||||
new_cache.append(decoder_new_cache)
|
||||
else:
|
||||
out = decoder_result[0]
|
||||
|
||||
out = self._norm(out)
|
||||
logits = self._linear(out)
|
||||
|
||||
# Возвращаем результат с учетом use_cache
|
||||
if use_cache:
|
||||
return (logits, new_cache)
|
||||
else:
|
||||
return (logits, None)
|
||||
|
||||
def generate(self,
|
||||
x: torch.Tensor,
|
||||
max_new_tokens: int,
|
||||
do_sample: bool,
|
||||
temperature: float = 1.0,
|
||||
top_k: int = None,
|
||||
top_p: float = None,
|
||||
use_cache: bool = True
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Авторегрессивная генерация токенов с поддержкой greedy, temperature, top-k/top-p sampling
|
||||
и ускорением через attention-кэш (KV-cache, важно для inference на длинных текстах).
|
||||
|
||||
Аргументы:
|
||||
x (torch.Tensor): Входной тензор с токенами shape [batch_size, seq_len].
|
||||
max_new_tokens (int): Максимальное количество новых токенов для генерации.
|
||||
do_sample (bool): Если True — вероятность/случайность (random sampling); если False — жадная генерация (argmax).
|
||||
temperature (float): Температура (>0, по умолчанию 1.0); >1.0 — более случайные выборы, <1.0 — более строгие.
|
||||
top_k (int, optional): top-k sampling; при сэмплировании выбираются только top_k наиболее вероятных токенов.
|
||||
top_p (float, optional): nucleus (top-p) sampling; выбираются токены с накопленной вероятностью ≤ top_p.
|
||||
use_cache (bool, по умолчанию True): Использовать ускорение через KV attention cache для autoregressive режима.
|
||||
|
||||
Возвращает:
|
||||
torch.Tensor: Последовательность индексов токенов shape [batch_size, seq_len + max_new_tokens].
|
||||
|
||||
Исключения:
|
||||
ValueError: Если x длиннее max_seq_len модели.
|
||||
ValueError: Если temperature ≤ 0.
|
||||
ValueError: Если одновременно заданы top_k и top_p.
|
||||
ValueError: Если top_k ≤ 0.
|
||||
ValueError: Если top_p не в диапазоне (0, 1].
|
||||
|
||||
Примеры:
|
||||
>>> # Жадная генерация
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=False)
|
||||
>>> # Сэмплирование с температурой
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, temperature=0.8)
|
||||
>>> # Top-k sampling
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, top_k=50)
|
||||
>>> # Top-p (nucleus) sampling
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, top_p=0.92)
|
||||
>>> # Температура + top-k
|
||||
>>> out = model.generate(input_ids, max_new_tokens=16, do_sample=True, temperature=1.0, top_k=100)
|
||||
|
||||
Примечания:
|
||||
- Одновременно использовать top_k и top_p нельзя.
|
||||
- Параметры temperature, top_k, top_p работают только при do_sample=True.
|
||||
- Для полного воспроизведения результата зафиксируйте seed через torch.manual_seed.
|
||||
- Метод всегда возвращает только индексы токенов; для получения логитов используйте forward.
|
||||
|
||||
Ссылки:
|
||||
- Holtzman et al., "The Curious Case of Neural Text Degeneration" (nucleus/top-p sampling): https://arxiv.org/abs/1904.09751
|
||||
- Mistral: https://arxiv.org/abs/2310.06825
|
||||
"""
|
||||
cache = None
|
||||
|
||||
for _ in range(max_new_tokens):
|
||||
if use_cache and cache is not None:
|
||||
# Используем кэш - передаем только последний токен
|
||||
x_input = x[:, -1:] # [batch_size, 1]
|
||||
else:
|
||||
# Первая итерация или кэш отключен - передаем всю последовательность
|
||||
x_input = x
|
||||
|
||||
# Прямой проход с кэшем
|
||||
logits, new_cache = self.forward(x_input, use_cache=use_cache, cache=cache)
|
||||
|
||||
# Обновляем кэш для следующей итерации
|
||||
if use_cache:
|
||||
cache = new_cache
|
||||
|
||||
last_logits = logits[:, -1, :] # [batch_size, vocab_size]
|
||||
|
||||
# Масштабируем логиты температурой
|
||||
if temperature > 0:
|
||||
logits_scaled = last_logits / temperature
|
||||
else:
|
||||
logits_scaled = last_logits
|
||||
|
||||
if do_sample == True and top_k != None:
|
||||
_, topk_indices = torch.topk(logits_scaled, top_k, dim=-1)
|
||||
|
||||
# # Заменим все НЕ top-k логиты на -inf
|
||||
masked_logits = logits_scaled.clone()
|
||||
vocab_size = logits_scaled.size(-1)
|
||||
|
||||
# создаём маску: 1, если токен НЕ в topk_indices
|
||||
mask = torch.ones_like(logits_scaled, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
mask.scatter_(1, topk_indices, False if hasattr(torch, "bool") else 0) # 0 там, где top-k индексы
|
||||
masked_logits[mask.bool() if hasattr(torch, "bool") else mask.byte()] = float('-inf')
|
||||
|
||||
logits_scaled = masked_logits
|
||||
|
||||
if do_sample == True and top_p != None:
|
||||
# 1. Применим softmax, чтобы получить вероятности:
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [B, vocab_size]
|
||||
# 2. Отсортируем токены по убыванию вероятностей:
|
||||
sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)
|
||||
# 3. Посчитаем кумулятивную сумму вероятностей:
|
||||
cum_probs = torch.cumsum(sorted_probs, dim=-1) # [B, vocab_size]
|
||||
# 4. Определим маску: оставить токены, пока сумма < top_p
|
||||
sorted_mask = (cum_probs <= top_p).bool() if hasattr(torch, "bool") else (cum_probs <= top_p).byte() # [B, vocab_size]
|
||||
# Гарантируем, что хотя бы первый токен останется
|
||||
sorted_mask[:, 0] = True if hasattr(torch, "bool") else 1
|
||||
# 5. Преобразуем маску обратно в оригинальный порядок:
|
||||
# Создаём полную маску из 0
|
||||
mask = torch.zeros_like(probs, dtype=torch.bool if hasattr(torch, "bool") else torch.uint8)
|
||||
# Устанавливаем 1 в местах нужных токенов
|
||||
mask.scatter_(dim=1, index=sorted_indices, src=sorted_mask)
|
||||
# 6. Зануляем логиты токенов вне топ-p:
|
||||
logits_scaled[~mask] = float('-inf')
|
||||
|
||||
# 4. Применяем Softmax
|
||||
probs = F.softmax(logits_scaled, dim=-1) # [batch_size, vocab_size]
|
||||
|
||||
|
||||
if do_sample == True:
|
||||
# 5. Если do_sample равен True, то отбираем токен случайно с помощью torch.multinomial
|
||||
next_token = torch.multinomial(probs, num_samples=1) # [batch_size, 1]
|
||||
else:
|
||||
# 5. Если do_sample равен False, то выбираем токен с максимальной вероятностью
|
||||
next_token = torch.argmax(probs, dim=-1, keepdim=True) # [batch_size, 1]
|
||||
|
||||
# 6. Добавляем его к последовательности
|
||||
x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1]
|
||||
return x
|
||||
|
||||
@property
|
||||
def max_seq_len(self) -> int:
|
||||
return self._max_seq_len
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,92 +10,94 @@ import json
|
||||
class BaseTokenizer(ABC):
|
||||
"""
|
||||
Абстрактный базовый класс для всех токенизаторов.
|
||||
|
||||
|
||||
Определяет общий интерфейс для токенизации текста.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.vocab: Dict[str, int] = {}
|
||||
self.inverse_vocab: Dict[int, str] = {}
|
||||
self.vocab_size: int = 0
|
||||
|
||||
|
||||
# Специальные токены
|
||||
self.pad_token = "<pad>"
|
||||
self.unk_token = "<unk>"
|
||||
self.bos_token = "<bos>"
|
||||
self.eos_token = "<eos>"
|
||||
|
||||
|
||||
self.pad_token_id: Optional[int] = None
|
||||
self.unk_token_id: Optional[int] = None
|
||||
self.bos_token_id: Optional[int] = None
|
||||
self.eos_token_id: Optional[int] = None
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||
"""
|
||||
Обучение токенизатора на текстах.
|
||||
|
||||
|
||||
Args:
|
||||
texts: Список текстов для обучения
|
||||
vocab_size: Желаемый размер словаря
|
||||
**kwargs: Дополнительные параметры обучения
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def encode(self, text: str, **kwargs) -> List[int]:
|
||||
"""
|
||||
Кодирование текста в последовательность токенов.
|
||||
|
||||
|
||||
Args:
|
||||
text: Входной текст
|
||||
**kwargs: Дополнительные параметры кодирования
|
||||
|
||||
|
||||
Returns:
|
||||
List[int]: Список идентификаторов токенов
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||
"""
|
||||
Декодирование последовательности токенов в текст.
|
||||
|
||||
|
||||
Args:
|
||||
tokens: Список идентификаторов токенов
|
||||
**kwargs: Дополнительные параметры декодирования
|
||||
|
||||
|
||||
Returns:
|
||||
str: Декодированный текст
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def tokenize(self, text: str, **kwargs) -> List[str]:
|
||||
"""
|
||||
Токенизация текста в список строковых токенов.
|
||||
|
||||
|
||||
Args:
|
||||
text: Входной текст
|
||||
**kwargs: Дополнительные параметры
|
||||
|
||||
|
||||
Returns:
|
||||
List[str]: Список токенов
|
||||
"""
|
||||
token_ids = self.encode(text, **kwargs)
|
||||
return [self.inverse_vocab.get(token_id, self.unk_token) for token_id in token_ids]
|
||||
|
||||
return [
|
||||
self.inverse_vocab.get(token_id, self.unk_token) for token_id in token_ids
|
||||
]
|
||||
|
||||
def get_vocab(self) -> Dict[str, int]:
|
||||
"""Возвращает словарь токенизатора."""
|
||||
return self.vocab.copy()
|
||||
|
||||
|
||||
def get_vocab_size(self) -> int:
|
||||
"""Возвращает размер словаря."""
|
||||
return self.vocab_size
|
||||
|
||||
|
||||
def add_special_tokens(self, special_tokens: List[str]):
|
||||
"""
|
||||
Добавляет специальные токены в словарь.
|
||||
|
||||
|
||||
Args:
|
||||
special_tokens: Список специальных токенов
|
||||
"""
|
||||
@@ -105,70 +107,70 @@ class BaseTokenizer(ABC):
|
||||
self.vocab[token] = token_id
|
||||
self.inverse_vocab[token_id] = token
|
||||
self.vocab_size += 1
|
||||
|
||||
|
||||
# Обновляем ID специальных токенов
|
||||
self.pad_token_id = self.vocab.get(self.pad_token)
|
||||
self.unk_token_id = self.vocab.get(self.unk_token)
|
||||
self.bos_token_id = self.vocab.get(self.bos_token)
|
||||
self.eos_token_id = self.vocab.get(self.eos_token)
|
||||
|
||||
|
||||
def save(self, filepath: str):
|
||||
"""
|
||||
Сохраняет токенизатор в файл.
|
||||
|
||||
|
||||
Args:
|
||||
filepath: Путь для сохранения
|
||||
"""
|
||||
config = {
|
||||
'vocab': self.vocab,
|
||||
'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,
|
||||
'tokenizer_type': self.__class__.__name__
|
||||
"vocab": self.vocab,
|
||||
"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,
|
||||
"tokenizer_type": self.__class__.__name__,
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
@classmethod
|
||||
def load(cls, filepath: str):
|
||||
"""
|
||||
Загружает токенизатор из файла.
|
||||
|
||||
|
||||
Args:
|
||||
filepath: Путь к файлу
|
||||
|
||||
|
||||
Returns:
|
||||
BaseTokenizer: Загруженный токенизатор
|
||||
"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
|
||||
# Создаем экземпляр токенизатора
|
||||
tokenizer = cls()
|
||||
tokenizer.vocab = config['vocab']
|
||||
tokenizer.vocab_size = config['vocab_size']
|
||||
tokenizer.pad_token = config['pad_token']
|
||||
tokenizer.unk_token = config['unk_token']
|
||||
tokenizer.bos_token = config['bos_token']
|
||||
tokenizer.eos_token = config['eos_token']
|
||||
|
||||
tokenizer.vocab = config["vocab"]
|
||||
tokenizer.vocab_size = config["vocab_size"]
|
||||
tokenizer.pad_token = config["pad_token"]
|
||||
tokenizer.unk_token = config["unk_token"]
|
||||
tokenizer.bos_token = config["bos_token"]
|
||||
tokenizer.eos_token = config["eos_token"]
|
||||
|
||||
# Создаем обратный словарь
|
||||
tokenizer.inverse_vocab = {v: k for k, v in tokenizer.vocab.items()}
|
||||
|
||||
|
||||
# Обновляем ID специальных токенов
|
||||
tokenizer.pad_token_id = tokenizer.vocab.get(tokenizer.pad_token)
|
||||
tokenizer.unk_token_id = tokenizer.vocab.get(tokenizer.unk_token)
|
||||
tokenizer.bos_token_id = tokenizer.vocab.get(tokenizer.bos_token)
|
||||
tokenizer.eos_token_id = tokenizer.vocab.get(tokenizer.eos_token)
|
||||
|
||||
|
||||
return tokenizer
|
||||
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Возвращает размер словаря."""
|
||||
return self.vocab_size
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(vocab_size={self.vocab_size})"
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
"""
|
||||
BPE (Byte Pair Encoding) токенизатор.
|
||||
|
||||
Реализация алгоритма BPE для токенизации текста.
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import defaultdict, Counter
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
from .base_tokenizer import BaseTokenizer
|
||||
|
||||
|
||||
class BPETokenizer(BaseTokenizer):
|
||||
"""
|
||||
BPE токенизатор для обработки текста.
|
||||
|
||||
Реализует алгоритм Byte Pair Encoding для создания субсловных токенов.
|
||||
|
||||
Примеры использования:
|
||||
>>> tokenizer = BPETokenizer()
|
||||
>>> tokenizer.train(["пример текста для обучения"], vocab_size=1000)
|
||||
>>> tokens = tokenizer.encode("новый текст")
|
||||
>>> text = tokenizer.decode(tokens)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.merges: Dict[Tuple[str, str], int] = {}
|
||||
self.pattern = r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
|
||||
self.compiled_pattern = re.compile(self.pattern, re.UNICODE)
|
||||
|
||||
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||
"""
|
||||
Обучение BPE токенизатора на текстах.
|
||||
|
||||
Args:
|
||||
texts: Список текстов для обучения
|
||||
vocab_size: Желаемый размер словаря
|
||||
**kwargs: Дополнительные параметры
|
||||
- min_frequency: Минимальная частота для мерджа
|
||||
- special_tokens: Список специальных токенов
|
||||
"""
|
||||
# Инициализация базового словаря
|
||||
self._initialize_vocab()
|
||||
|
||||
# Добавляем специальные токены если указаны
|
||||
special_tokens = kwargs.get('special_tokens', [self.pad_token, self.unk_token, self.bos_token, self.eos_token])
|
||||
self.add_special_tokens(special_tokens)
|
||||
|
||||
# Предобработка текстов
|
||||
words = self._preprocess_texts(texts)
|
||||
|
||||
# Получаем начальные токены
|
||||
vocab = self._get_initial_vocab(words)
|
||||
|
||||
# Выполняем BPE мерджи
|
||||
self._perform_merges(vocab, vocab_size, kwargs.get('min_frequency', 2))
|
||||
|
||||
# Строим финальный словарь
|
||||
self._build_final_vocab()
|
||||
|
||||
def _initialize_vocab(self):
|
||||
"""Инициализирует базовый словарь."""
|
||||
self.vocab.clear()
|
||||
self.inverse_vocab.clear()
|
||||
self.merges.clear()
|
||||
self.vocab_size = 0
|
||||
|
||||
def _preprocess_texts(self, texts: List[str]) -> List[List[str]]:
|
||||
"""
|
||||
Предобработка текстов для обучения.
|
||||
|
||||
Args:
|
||||
texts: Список текстов
|
||||
|
||||
Returns:
|
||||
List[List[str]]: Предобработанные слова
|
||||
"""
|
||||
words = []
|
||||
for text in texts:
|
||||
# Базовая нормализация
|
||||
text = text.lower().strip()
|
||||
# Токенизация на слова
|
||||
tokens = self.compiled_pattern.findall(text)
|
||||
words.append(tokens)
|
||||
return words
|
||||
|
||||
def _get_initial_vocab(self, words: List[List[str]]) -> Dict[str, int]:
|
||||
"""
|
||||
Создает начальный словарь из символов.
|
||||
|
||||
Args:
|
||||
words: Список токенизированных текстов
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Начальный словарь частот
|
||||
"""
|
||||
vocab = Counter()
|
||||
for word_list in words:
|
||||
for word in word_list:
|
||||
# Разбиваем слово на символы и добавляем специальный символ конца слова
|
||||
chars = list(word) + ['</w>']
|
||||
vocab.update([''.join(chars[i:i+1]) for i in range(len(chars))])
|
||||
return vocab
|
||||
|
||||
def _perform_merges(self, vocab: Dict[str, int], target_vocab_size: int, min_frequency: int):
|
||||
"""
|
||||
Выполняет BPE мерджи до достижения целевого размера словаря.
|
||||
|
||||
Args:
|
||||
vocab: Начальный словарь
|
||||
target_vocab_size: Целевой размер словаря
|
||||
min_frequency: Минимальная частота для мерджа
|
||||
"""
|
||||
current_vocab_size = len(vocab) + len(self.vocab)
|
||||
|
||||
while current_vocab_size < target_vocab_size:
|
||||
# Находим наиболее частую пару
|
||||
pairs = self._get_stats(vocab)
|
||||
if not pairs:
|
||||
break
|
||||
|
||||
best_pair = max(pairs, key=pairs.get)
|
||||
if pairs[best_pair] < min_frequency:
|
||||
break
|
||||
|
||||
# Выполняем мердж
|
||||
vocab = self._merge_vocab(vocab, best_pair)
|
||||
self.merges[best_pair] = len(self.merges)
|
||||
current_vocab_size += 1
|
||||
|
||||
def _get_stats(self, vocab: Dict[str, int]) -> Dict[Tuple[str, str], int]:
|
||||
"""
|
||||
Собирает статистику по парам символов.
|
||||
|
||||
Args:
|
||||
vocab: Словарь токенов
|
||||
|
||||
Returns:
|
||||
Dict[Tuple[str, str], int]: Частоты пар
|
||||
"""
|
||||
pairs = defaultdict(int)
|
||||
for word, freq in vocab.items():
|
||||
symbols = word.split()
|
||||
for i in range(len(symbols) - 1):
|
||||
pairs[symbols[i], symbols[i + 1]] += freq
|
||||
return pairs
|
||||
|
||||
def _merge_vocab(self, vocab: Dict[str, int], pair: Tuple[str, str]) -> Dict[str, int]:
|
||||
"""
|
||||
Объединяет пару символов в словаре.
|
||||
|
||||
Args:
|
||||
vocab: Исходный словарь
|
||||
pair: Пара для объединения
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Обновленный словарь
|
||||
"""
|
||||
new_vocab = {}
|
||||
bigram = re.compile(r'(?<!\\S)' + re.escape(pair[0]) + r' ' + re.escape(pair[1]) + r'(?!\\S)')
|
||||
replacement = pair[0] + pair[1]
|
||||
|
||||
for word in vocab:
|
||||
new_word = bigram.sub(replacement, word)
|
||||
new_vocab[new_word] = vocab[word]
|
||||
|
||||
return new_vocab
|
||||
|
||||
def _build_final_vocab(self):
|
||||
"""Строит финальный словарь токенизатора."""
|
||||
# Собираем все уникальные токены из мерджей
|
||||
all_tokens = set()
|
||||
|
||||
# Добавляем специальные токены
|
||||
all_tokens.update([self.pad_token, self.unk_token, self.bos_token, self.eos_token])
|
||||
|
||||
# Добавляем токены из мерджей
|
||||
for pair in self.merges:
|
||||
all_tokens.update(pair)
|
||||
|
||||
# Создаем словарь
|
||||
for i, token in enumerate(sorted(all_tokens)):
|
||||
self.vocab[token] = i
|
||||
|
||||
self.inverse_vocab = {v: k for k, v in self.vocab.items()}
|
||||
self.vocab_size = len(self.vocab)
|
||||
|
||||
# Обновляем ID специальных токенов
|
||||
self.pad_token_id = self.vocab.get(self.pad_token)
|
||||
self.unk_token_id = self.vocab.get(self.unk_token)
|
||||
self.bos_token_id = self.vocab.get(self.bos_token)
|
||||
self.eos_token_id = self.vocab.get(self.eos_token)
|
||||
|
||||
def encode(self, text: str, **kwargs) -> List[int]:
|
||||
"""
|
||||
Кодирует текст в последовательность токенов.
|
||||
|
||||
Args:
|
||||
text: Входной текст
|
||||
**kwargs: Дополнительные параметры
|
||||
- add_special_tokens: Добавлять специальные токены
|
||||
|
||||
Returns:
|
||||
List[int]: Список идентификаторов токенов
|
||||
"""
|
||||
add_special_tokens = kwargs.get('add_special_tokens', False)
|
||||
|
||||
# Токенизация текста
|
||||
tokens = self.compiled_pattern.findall(text)
|
||||
|
||||
# Применяем BPE к каждому токену
|
||||
bpe_tokens = []
|
||||
for token in tokens:
|
||||
# Преобразуем токен в BPE представление
|
||||
bpe_token = self._apply_bpe(token)
|
||||
bpe_tokens.extend(bpe_token)
|
||||
|
||||
# Конвертируем в ID
|
||||
token_ids = []
|
||||
for token in bpe_tokens:
|
||||
token_id = self.vocab.get(token, self.unk_token_id)
|
||||
if token_id is not None:
|
||||
token_ids.append(token_id)
|
||||
|
||||
# Добавляем специальные токены если нужно
|
||||
if add_special_tokens:
|
||||
if self.bos_token_id is not None:
|
||||
token_ids.insert(0, self.bos_token_id)
|
||||
if self.eos_token_id is not None:
|
||||
token_ids.append(self.eos_token_id)
|
||||
|
||||
return token_ids
|
||||
|
||||
def _apply_bpe(self, token: str) -> List[str]:
|
||||
"""
|
||||
Применяет BPE к одному токену.
|
||||
|
||||
Args:
|
||||
token: Входной токен
|
||||
|
||||
Returns:
|
||||
List[str]: Список BPE токенов
|
||||
"""
|
||||
# Простая реализация - в реальной реализации нужно применять обученные мерджи
|
||||
word = token + '</w>'
|
||||
tokens = [word[i:i+1] for i in range(len(word))]
|
||||
|
||||
# Применяем мерджи (упрощенная версия)
|
||||
# В полной реализации нужно применять все обученные мерджи
|
||||
for pair in self.merges:
|
||||
i = 0
|
||||
while i < len(tokens) - 1:
|
||||
if tokens[i] == pair[0] and tokens[i + 1] == pair[1]:
|
||||
tokens[i] = tokens[i] + tokens[i + 1]
|
||||
del tokens[i + 1]
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return tokens
|
||||
|
||||
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||
"""
|
||||
Декодирует последовательность токенов в текст.
|
||||
|
||||
Args:
|
||||
tokens: Список идентификаторов токенов
|
||||
**kwargs: Дополнительные параметры
|
||||
- skip_special_tokens: Пропускать специальные токены
|
||||
|
||||
Returns:
|
||||
str: Декодированный текст
|
||||
"""
|
||||
skip_special_tokens = kwargs.get('skip_special_tokens', True)
|
||||
|
||||
# Конвертируем ID в токены
|
||||
token_strings = []
|
||||
for token_id in tokens:
|
||||
token = self.inverse_vocab.get(token_id, self.unk_token)
|
||||
|
||||
# Пропускаем специальные токены если нужно
|
||||
if skip_special_tokens and token in [self.pad_token, self.unk_token, self.bos_token, self.eos_token]:
|
||||
continue
|
||||
|
||||
token_strings.append(token)
|
||||
|
||||
# Объединяем токены в текст
|
||||
text = ''.join(token_strings)
|
||||
|
||||
# Убираем маркер конца слова
|
||||
text = text.replace('</w>', ' ')
|
||||
|
||||
return text.strip()
|
||||
|
||||
def save(self, filepath: str):
|
||||
"""
|
||||
Сохраняет BPE токенизатор в файл.
|
||||
|
||||
Args:
|
||||
filepath: Путь для сохранения
|
||||
"""
|
||||
import json
|
||||
|
||||
config = {
|
||||
'vocab': self.vocab,
|
||||
'merges': {f"{k[0]} {k[1]}": v for k, v in self.merges.items()},
|
||||
'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,
|
||||
'pattern': self.pattern,
|
||||
'tokenizer_type': self.__class__.__name__
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
@classmethod
|
||||
def load(cls, filepath: str):
|
||||
"""
|
||||
Загружает BPE токенизатор из файла.
|
||||
|
||||
Args:
|
||||
filepath: Путь к файлу
|
||||
|
||||
Returns:
|
||||
BPETokenizer: Загруженный токенизатор
|
||||
"""
|
||||
import json
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
tokenizer = cls()
|
||||
tokenizer.vocab = config['vocab']
|
||||
tokenizer.vocab_size = config['vocab_size']
|
||||
tokenizer.pad_token = config['pad_token']
|
||||
tokenizer.unk_token = config['unk_token']
|
||||
tokenizer.bos_token = config['bos_token']
|
||||
tokenizer.eos_token = config['eos_token']
|
||||
tokenizer.pattern = config.get('pattern', tokenizer.pattern)
|
||||
tokenizer.compiled_pattern = re.compile(tokenizer.pattern, re.UNICODE)
|
||||
|
||||
# Восстанавливаем мерджи
|
||||
merges = config.get('merges', {})
|
||||
tokenizer.merges = {}
|
||||
for k, v in merges.items():
|
||||
parts = k.split()
|
||||
if len(parts) == 2:
|
||||
tokenizer.merges[(parts[0], parts[1])] = v
|
||||
|
||||
# Создаем обратный словарь
|
||||
tokenizer.inverse_vocab = {v: k for k, v in tokenizer.vocab.items()}
|
||||
|
||||
# Обновляем ID специальных токенов
|
||||
tokenizer.pad_token_id = tokenizer.vocab.get(tokenizer.pad_token)
|
||||
tokenizer.unk_token_id = tokenizer.vocab.get(tokenizer.unk_token)
|
||||
tokenizer.bos_token_id = tokenizer.vocab.get(tokenizer.bos_token)
|
||||
tokenizer.eos_token_id = tokenizer.vocab.get(tokenizer.eos_token)
|
||||
|
||||
return tokenizer
|
||||
|
||||
|
||||
# Упрощенная версия для быстрого старта
|
||||
class SimpleBPETokenizer(BPETokenizer):
|
||||
"""
|
||||
Упрощенная версия BPE токенизатора для демонстрации.
|
||||
"""
|
||||
|
||||
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||
"""Упрощенное обучение для демонстрации."""
|
||||
# Инициализация базового словаря
|
||||
self._initialize_vocab()
|
||||
|
||||
# Добавляем базовые токены
|
||||
special_tokens = [self.pad_token, self.unk_token, self.bos_token, self.eos_token]
|
||||
self.add_special_tokens(special_tokens)
|
||||
|
||||
# Простая реализация - собираем все символы
|
||||
all_chars = set()
|
||||
for text in texts:
|
||||
all_chars.update(text)
|
||||
|
||||
# Добавляем символы в словарь
|
||||
for char in sorted(all_chars):
|
||||
if char not in self.vocab:
|
||||
self.vocab[char] = len(self.vocab)
|
||||
|
||||
self.inverse_vocab = {v: k for k, v in self.vocab.items()}
|
||||
self.vocab_size = len(self.vocab)
|
||||
|
||||
# Обновляем ID специальных токенов
|
||||
self.pad_token_id = self.vocab.get(self.pad_token)
|
||||
self.unk_token_id = self.vocab.get(self.unk_token)
|
||||
self.bos_token_id = self.vocab.get(self.bos_token)
|
||||
self.eos_token_id = self.vocab.get(self.eos_token)
|
||||
|
||||
def encode(self, text: str, **kwargs) -> List[int]:
|
||||
"""Упрощенное кодирование - разбиваем на символы."""
|
||||
add_special_tokens = kwargs.get('add_special_tokens', False)
|
||||
|
||||
token_ids = []
|
||||
for char in text:
|
||||
token_id = self.vocab.get(char, self.unk_token_id)
|
||||
if token_id is not None:
|
||||
token_ids.append(token_id)
|
||||
|
||||
if add_special_tokens:
|
||||
if self.bos_token_id is not None:
|
||||
token_ids.insert(0, self.bos_token_id)
|
||||
if self.eos_token_id is not None:
|
||||
token_ids.append(self.eos_token_id)
|
||||
|
||||
return token_ids
|
||||
|
||||
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||
"""Упрощенное декодирование."""
|
||||
skip_special_tokens = kwargs.get('skip_special_tokens', True)
|
||||
|
||||
chars = []
|
||||
for token_id in tokens:
|
||||
char = self.inverse_vocab.get(token_id, self.unk_token)
|
||||
if skip_special_tokens and char in [self.pad_token, self.unk_token, self.bos_token, self.eos_token]:
|
||||
continue
|
||||
chars.append(char)
|
||||
|
||||
return ''.join(chars)
|
||||
@@ -10,27 +10,65 @@ from .base_tokenizer import BaseTokenizer
|
||||
|
||||
class BPETokenizer(BaseTokenizer):
|
||||
"""
|
||||
BPE токенизатор для обработки текста.
|
||||
|
||||
Реализует алгоритм Byte Pair Encoding для создания субсловных токенов.
|
||||
Использует вашу реализацию BPE.
|
||||
|
||||
Примеры использования:
|
||||
>>> tokenizer = BPETokenizer()
|
||||
>>> tokenizer.train(["пример текста для обучения"], vocab_size=1000)
|
||||
>>> tokens = tokenizer.encode("новый текст")
|
||||
BpeTokenizer — реализация токенизатора на алгоритме byte pair encoding (BPE).
|
||||
|
||||
Назначение:
|
||||
-----------
|
||||
- Преобразует открытый текст (строки, bytes) в последовательность числовых токенов для подачи в LLM и обратно.
|
||||
- Разбивает текст на сабслова (байтовые пары), эффективно кодируя редкие слова длинными последовательностями, а частые — единичными токенами.
|
||||
- Является стандартом де-факто в современных языковых моделях (GPT, LLaMA, BLOOM, Mistral, HuggingFace).
|
||||
|
||||
Как работает BPE:
|
||||
-----------------
|
||||
1. Строится словарь из наиболее популярных пар символов/субстрок.
|
||||
2. Текст замещается наиболее длинными subword-подстроками из vocabulary (жадно).
|
||||
3. Итог: многомиллионное лексическое пространство сокращается до компактного набора subword pieces.
|
||||
|
||||
Особенности алгоритма:
|
||||
----------------------
|
||||
- Отлично работает на всех языках, включая rare/compound/inflectable.
|
||||
- Гибко масштабируется под размер итогового словаря/token space.
|
||||
- Обычно хранит mapping (str/bytes → int и int → str/bytes) в JSON или словарном файле.
|
||||
- Может использовать кастомные сепараторы, handle unknown.
|
||||
|
||||
Аргументы конструктора:
|
||||
-----------------------
|
||||
vocab_path: str
|
||||
Путь к файлу BPE vocabulary (JSON, txt, в зависимости от реализации).
|
||||
merges_path: str, optional
|
||||
Путь к списку merge-правил (если используется блочное файловое раздельное хранение).
|
||||
unk_token: str, optional
|
||||
Токен для неизвестных последовательностей (по дефолту '[UNK]' или '<unk>').
|
||||
pad_token, bos_token, eos_token: str, optional
|
||||
Special tokens, если нужны для вашей архитектуры.
|
||||
lowercase: bool, optional
|
||||
Приводить ли текст к нижнему регистру перед токенизацией.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> tokenizer = BpeTokenizer(vocab_path=\"bpe_vocab.json\")
|
||||
>>> tokens = tokenizer.encode(\"Hello, world!\")
|
||||
>>> print(tokens) # [15496, 11, ...]
|
||||
>>> text = tokenizer.decode(tokens)
|
||||
>>> print(text) # 'Hello, world!'
|
||||
|
||||
References:
|
||||
-----------
|
||||
- Sennrich et al, \"Neural Machine Translation of Rare Words with Subword Units\", 2015: https://arxiv.org/abs/1508.07909
|
||||
- GPT-2 tokenization: https://github.com/openai/gpt-2
|
||||
- HuggingFace tokenizers overview: https://huggingface.co/docs/tokenizers/index
|
||||
- Visually: https://guillaume-be.github.io/2021-05-21/byte-pair-encoding/
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.merges: Dict[Tuple[str, str], int] = {}
|
||||
self.vocab_list: List[str] = []
|
||||
|
||||
|
||||
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||
"""
|
||||
Обучение BPE токенизатора на текстах.
|
||||
|
||||
|
||||
Args:
|
||||
texts: Список текстов для обучения
|
||||
vocab_size: Желаемый размер словаря
|
||||
@@ -39,7 +77,7 @@ class BPETokenizer(BaseTokenizer):
|
||||
"""
|
||||
# Объединяем все тексты в одну строку для обучения
|
||||
combined_text = " ".join(texts)
|
||||
|
||||
|
||||
# 1. Получаем уникальные токены (символы)
|
||||
unique_tokens = sorted(set(combined_text))
|
||||
tokens = unique_tokens.copy()
|
||||
@@ -61,7 +99,10 @@ class BPETokenizer(BaseTokenizer):
|
||||
break # нет пар — выходим
|
||||
|
||||
# Находим самую частую пару (в случае равенства — та, что встретилась первой)
|
||||
most_frequent_pair = max(pair_freq.items(), key=lambda x: (x[1], -self._pair_first_index(sequence, x[0])))[0]
|
||||
most_frequent_pair = max(
|
||||
pair_freq.items(),
|
||||
key=lambda x: (x[1], -self._pair_first_index(sequence, x[0])),
|
||||
)[0]
|
||||
|
||||
# Создаем новый токен
|
||||
new_token = most_frequent_pair[0] + most_frequent_pair[1]
|
||||
@@ -71,45 +112,57 @@ class BPETokenizer(BaseTokenizer):
|
||||
new_sequence = []
|
||||
|
||||
while i < len(sequence):
|
||||
if i < len(sequence) - 1 and (sequence[i], sequence[i + 1]) == most_frequent_pair:
|
||||
if (
|
||||
i < len(sequence) - 1
|
||||
and (sequence[i], sequence[i + 1]) == most_frequent_pair
|
||||
):
|
||||
new_sequence.append(new_token)
|
||||
i += 2 # пропускаем два символа — заменённую пару
|
||||
else:
|
||||
new_sequence.append(sequence[i])
|
||||
i += 1
|
||||
sequence = new_sequence
|
||||
|
||||
|
||||
# 4. Создаем словари
|
||||
self.vocab_list = tokens.copy()
|
||||
self.vocab = dict(zip(tokens, range(vocab_size)))
|
||||
self.inverse_vocab = dict(zip(range(vocab_size), tokens))
|
||||
self.vocab_size = len(self.vocab)
|
||||
|
||||
|
||||
# Добавляем специальные токены если указаны
|
||||
special_tokens = kwargs.get('special_tokens', [self.pad_token, self.unk_token, self.bos_token, self.eos_token])
|
||||
special_tokens = kwargs.get(
|
||||
"special_tokens",
|
||||
[self.pad_token, self.unk_token, self.bos_token, self.eos_token],
|
||||
)
|
||||
self.add_special_tokens(special_tokens)
|
||||
|
||||
|
||||
def _pair_first_index(self, sequence, pair):
|
||||
"""Находит первый индекс пары в последовательности."""
|
||||
for i in range(len(sequence) - 1):
|
||||
if (sequence[i], sequence[i + 1]) == pair:
|
||||
return i
|
||||
return float('inf') # если пара не найдена (в теории не должно случиться)
|
||||
return float("inf") # если пара не найдена (в теории не должно случиться)
|
||||
|
||||
def encode(self, text: str, **kwargs) -> List[int]:
|
||||
"""
|
||||
Кодирует текст в последовательность токенов.
|
||||
|
||||
Токенизирует входной текст в список числовых токенов (индексов).
|
||||
|
||||
Args:
|
||||
text: Входной текст
|
||||
**kwargs: Дополнительные параметры
|
||||
- add_special_tokens: Добавлять специальные токены
|
||||
|
||||
-----
|
||||
text: str
|
||||
Входная строка/текст для токенизации.
|
||||
|
||||
Returns:
|
||||
List[int]: Список идентификаторов токенов
|
||||
--------
|
||||
List[int] — последовательность индексов из vocabulary.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> ids = tokenizer.encode(\"The quick brown fox\")
|
||||
>>> print(ids)
|
||||
"""
|
||||
add_special_tokens = kwargs.get('add_special_tokens', False)
|
||||
|
||||
add_special_tokens = kwargs.get("add_special_tokens", False)
|
||||
|
||||
# 1. Разбиваем текст на токены-символы
|
||||
sequence = list(text)
|
||||
# 2. Инициализация пустого списка токенов
|
||||
@@ -119,7 +172,9 @@ class BPETokenizer(BaseTokenizer):
|
||||
while i < len(text):
|
||||
# 3.1 Найти все токены в словаре, начинающиеся с text[i]
|
||||
start_char = text[i]
|
||||
result = [token for token in self.vocab_list if token.startswith(start_char)]
|
||||
result = [
|
||||
token for token in self.vocab_list if token.startswith(start_char)
|
||||
]
|
||||
# 3.2 Выбрать самый длинный подходящий токен
|
||||
find_token = self._find_max_matching_token(text[i:], result)
|
||||
if find_token is None:
|
||||
@@ -134,19 +189,19 @@ class BPETokenizer(BaseTokenizer):
|
||||
|
||||
# 4. Заменить токены на их ID
|
||||
token_ids = self._tokens_to_ids(tokens)
|
||||
|
||||
|
||||
# Заменяем -1 на unk_token_id
|
||||
token_ids = [tid if tid != -1 else self.unk_token_id for tid in token_ids]
|
||||
|
||||
|
||||
# Добавляем специальные токены если нужно
|
||||
if add_special_tokens:
|
||||
if self.bos_token_id is not None:
|
||||
token_ids.insert(0, self.bos_token_id)
|
||||
if self.eos_token_id is not None:
|
||||
token_ids.append(self.eos_token_id)
|
||||
|
||||
|
||||
return token_ids
|
||||
|
||||
|
||||
def _find_max_matching_token(self, text: str, tokens: list) -> Optional[str]:
|
||||
"""Находит самый длинный токен из списка, с которого начинается текст"""
|
||||
matching = [token for token in tokens if text.startswith(token)]
|
||||
@@ -161,33 +216,48 @@ class BPETokenizer(BaseTokenizer):
|
||||
else:
|
||||
ids.append(-1) # Специальное значение
|
||||
return ids
|
||||
|
||||
|
||||
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||
"""
|
||||
Декодирует последовательность токенов в текст.
|
||||
|
||||
Декодирует последовательность токенов обратно в текстовую строку.
|
||||
|
||||
Args:
|
||||
tokens: Список идентификаторов токенов
|
||||
**kwargs: Дополнительные параметры
|
||||
- skip_special_tokens: Пропускать специальные токены
|
||||
|
||||
-----
|
||||
ids: List[int]
|
||||
Список токен-индексов для распаковки.
|
||||
|
||||
Returns:
|
||||
str: Декодированный текст
|
||||
--------
|
||||
text: str
|
||||
Оригинальный (или приближённый) раскодированный текст.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> tokens = [15496, 11, 318, ...]
|
||||
>>> text = tokenizer.decode(tokens)
|
||||
"""
|
||||
skip_special_tokens = kwargs.get('skip_special_tokens', True)
|
||||
|
||||
skip_special_tokens = kwargs.get("skip_special_tokens", True)
|
||||
|
||||
# Фильтруем специальные токены если нужно
|
||||
if skip_special_tokens:
|
||||
tokens = [tid for tid in tokens if tid not in [
|
||||
self.pad_token_id, self.unk_token_id, self.bos_token_id, self.eos_token_id
|
||||
]]
|
||||
|
||||
tokens = [
|
||||
tid
|
||||
for tid in tokens
|
||||
if tid
|
||||
not in [
|
||||
self.pad_token_id,
|
||||
self.unk_token_id,
|
||||
self.bos_token_id,
|
||||
self.eos_token_id,
|
||||
]
|
||||
]
|
||||
|
||||
# Конвертируем ID в токены
|
||||
token_strings = self._ids_to_tokens(tokens)
|
||||
|
||||
|
||||
# Объединяем токены в текст
|
||||
return ''.join(token_strings)
|
||||
|
||||
return "".join(token_strings)
|
||||
|
||||
def _ids_to_tokens(self, ids: List[int]) -> List[str]:
|
||||
"""Конвертирует список Ids в их tokens"""
|
||||
tokens = []
|
||||
@@ -197,76 +267,76 @@ class BPETokenizer(BaseTokenizer):
|
||||
else:
|
||||
tokens.append(self.unk_token) # Специальное значение
|
||||
return tokens
|
||||
|
||||
|
||||
def save(self, filepath: str):
|
||||
"""
|
||||
Сохраняет токенизатор в файл.
|
||||
|
||||
|
||||
Args:
|
||||
filepath: Путь для сохранения
|
||||
"""
|
||||
import json
|
||||
|
||||
|
||||
# Преобразуем кортежи в строки для JSON сериализации
|
||||
merges_serializable = {f"{k[0]},{k[1]}": v for k, v in self.merges.items()}
|
||||
|
||||
|
||||
config = {
|
||||
'vocab': self.vocab,
|
||||
'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,
|
||||
'tokenizer_type': self.__class__.__name__,
|
||||
'merges': merges_serializable,
|
||||
'vocab_list': self.vocab_list
|
||||
"vocab": self.vocab,
|
||||
"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,
|
||||
"tokenizer_type": self.__class__.__name__,
|
||||
"merges": merges_serializable,
|
||||
"vocab_list": self.vocab_list,
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
@classmethod
|
||||
def load(cls, filepath: str):
|
||||
"""
|
||||
Загружает токенизатор из файла.
|
||||
|
||||
|
||||
Args:
|
||||
filepath: Путь к файлу
|
||||
|
||||
|
||||
Returns:
|
||||
BPETokenizer: Загруженный токенизатор
|
||||
"""
|
||||
import json
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
|
||||
# Создаем экземпляр токенизатора
|
||||
tokenizer = cls()
|
||||
tokenizer.vocab = config['vocab']
|
||||
tokenizer.vocab_size = config['vocab_size']
|
||||
tokenizer.pad_token = config['pad_token']
|
||||
tokenizer.unk_token = config['unk_token']
|
||||
tokenizer.bos_token = config['bos_token']
|
||||
tokenizer.eos_token = config['eos_token']
|
||||
tokenizer.vocab_list = config['vocab_list']
|
||||
|
||||
tokenizer.vocab = config["vocab"]
|
||||
tokenizer.vocab_size = config["vocab_size"]
|
||||
tokenizer.pad_token = config["pad_token"]
|
||||
tokenizer.unk_token = config["unk_token"]
|
||||
tokenizer.bos_token = config["bos_token"]
|
||||
tokenizer.eos_token = config["eos_token"]
|
||||
tokenizer.vocab_list = config["vocab_list"]
|
||||
|
||||
# Восстанавливаем кортежи из строк
|
||||
tokenizer.merges = {}
|
||||
for k, v in config['merges'].items():
|
||||
parts = k.split(',')
|
||||
for k, v in config["merges"].items():
|
||||
parts = k.split(",")
|
||||
if len(parts) == 2:
|
||||
tokenizer.merges[(parts[0], parts[1])] = v
|
||||
|
||||
|
||||
# Создаем обратный словарь
|
||||
tokenizer.inverse_vocab = {v: k for k, v in tokenizer.vocab.items()}
|
||||
|
||||
|
||||
# Обновляем ID специальных токенов
|
||||
tokenizer.pad_token_id = tokenizer.vocab.get(tokenizer.pad_token)
|
||||
tokenizer.unk_token_id = tokenizer.vocab.get(tokenizer.unk_token)
|
||||
tokenizer.bos_token_id = tokenizer.vocab.get(tokenizer.bos_token)
|
||||
tokenizer.eos_token_id = tokenizer.vocab.get(tokenizer.eos_token)
|
||||
|
||||
|
||||
return tokenizer
|
||||
|
||||
|
||||
@@ -275,4 +345,5 @@ class SimpleBPETokenizer(BPETokenizer):
|
||||
Упрощенная версия BPE токенизатора для демонстрации.
|
||||
Наследует вашу реализацию, но может быть упрощена при необходимости.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from typing import List, Any
|
||||
|
||||
|
||||
class TextDataset(Dataset):
|
||||
"""
|
||||
Простой датасет для языкового моделирования (LLM).
|
||||
Работает с любым токенизатором, реализующим интерфейс BaseTokenizer.
|
||||
"""
|
||||
|
||||
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128):
|
||||
"""
|
||||
Инициализация датасета.
|
||||
|
||||
Args:
|
||||
texts: Список текстов для обучения
|
||||
tokenizer: Токенизатор с методами encode/decode
|
||||
block_size: Максимальная длина последовательности
|
||||
"""
|
||||
self.examples = []
|
||||
self.tokenizer = tokenizer
|
||||
self.block_size = block_size
|
||||
|
||||
for text in texts:
|
||||
# Кодируем текст в токены
|
||||
input_ids = tokenizer.encode(text, add_special_tokens=False)
|
||||
|
||||
# Обрезаем или дополняем до нужной длины
|
||||
if len(input_ids) > block_size:
|
||||
input_ids = input_ids[:block_size]
|
||||
else:
|
||||
# Дополняем pad_token_id
|
||||
pad_token_id = getattr(tokenizer, 'pad_token_id', 0)
|
||||
input_ids = input_ids + [pad_token_id] * (block_size - len(input_ids))
|
||||
|
||||
self.examples.append(input_ids)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.examples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
input_ids = torch.tensor(self.examples[idx], dtype=torch.long)
|
||||
labels = input_ids.clone()
|
||||
return {"input_ids": input_ids, "labels": labels}
|
||||
|
||||
|
||||
class StreamingTextDataset(Dataset):
|
||||
"""
|
||||
Датасет для потоковой обработки больших текстов.
|
||||
Токенизация происходит на лету, что экономит память.
|
||||
"""
|
||||
|
||||
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128):
|
||||
self.texts = texts
|
||||
self.tokenizer = tokenizer
|
||||
self.block_size = block_size
|
||||
|
||||
# Получаем pad_token_id из токенизатора
|
||||
self.pad_token_id = getattr(tokenizer, 'pad_token_id', 0)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.texts)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
text = self.texts[idx]
|
||||
|
||||
# Токенизация на лету
|
||||
input_ids = self.tokenizer.encode(text, add_special_tokens=False)
|
||||
|
||||
# Обрезаем или дополняем до нужной длины
|
||||
if len(input_ids) > self.block_size:
|
||||
input_ids = input_ids[:self.block_size]
|
||||
else:
|
||||
input_ids = input_ids + [self.pad_token_id] * (self.block_size - len(input_ids))
|
||||
|
||||
input_ids = torch.tensor(input_ids, dtype=torch.long)
|
||||
labels = input_ids.clone()
|
||||
|
||||
return {"input_ids": input_ids, "labels": labels}
|
||||
|
||||
|
||||
class TextDatasetWithSpecialTokens(TextDataset):
|
||||
"""
|
||||
Расширенная версия TextDataset с поддержкой специальных токенов.
|
||||
"""
|
||||
|
||||
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128,
|
||||
add_bos: bool = False, add_eos: bool = False):
|
||||
"""
|
||||
Args:
|
||||
texts: Список текстов
|
||||
tokenizer: Токенизатор
|
||||
block_size: Максимальная длина
|
||||
add_bos: Добавлять токен начала последовательности
|
||||
add_eos: Добавлять токен конца последовательности
|
||||
"""
|
||||
self.examples = []
|
||||
self.tokenizer = tokenizer
|
||||
self.block_size = block_size
|
||||
self.add_bos = add_bos
|
||||
self.add_eos = add_eos
|
||||
|
||||
for text in texts:
|
||||
# Кодируем с специальными токенами
|
||||
input_ids = tokenizer.encode(
|
||||
text,
|
||||
add_special_tokens=True,
|
||||
add_bos_token=add_bos,
|
||||
add_eos_token=eos
|
||||
)
|
||||
|
||||
# Учитываем специальные токены при обрезке/дополнении
|
||||
effective_block_size = block_size
|
||||
if add_bos:
|
||||
effective_block_size -= 1
|
||||
if add_eos:
|
||||
effective_block_size -= 1
|
||||
|
||||
if len(input_ids) > effective_block_size:
|
||||
input_ids = input_ids[:effective_block_size]
|
||||
|
||||
# Добавляем специальные токены если нужно
|
||||
if add_bos and hasattr(tokenizer, 'bos_token_id') and tokenizer.bos_token_id is not None:
|
||||
input_ids = [tokenizer.bos_token_id] + input_ids
|
||||
if add_eos and hasattr(tokenizer, 'eos_token_id') and tokenizer.eos_token_id is not None:
|
||||
input_ids = input_ids + [tokenizer.eos_token_id]
|
||||
|
||||
# Дополняем до полной длины
|
||||
pad_token_id = getattr(tokenizer, 'pad_token_id', 0)
|
||||
if len(input_ids) < block_size:
|
||||
input_ids = input_ids + [pad_token_id] * (block_size - len(input_ids))
|
||||
|
||||
self.examples.append(input_ids)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.examples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
input_ids = torch.tensor(self.examples[idx], dtype=torch.long)
|
||||
labels = input_ids.clone()
|
||||
return {"input_ids": input_ids, "labels": labels}
|
||||
@@ -1,8 +1,71 @@
|
||||
"""
|
||||
Модуль оптимизации для обучения нейронных сетей.
|
||||
|
||||
В данном модуле реализована функция выбора и инициализации оптимизаторов, наиболее популярных при обучении глубоких нейросетей:
|
||||
- AdamW
|
||||
- Adam
|
||||
- SGD
|
||||
|
||||
Теоретическое обоснование:
|
||||
--------------------------
|
||||
Задача оптимизации в обучении нейросети заключается в минимизации функции потерь (Loss) по параметрам модели W. Современные методы базируются на стохастическом градиентном спуске (SGD), а также на его адаптивных модификациях (Adam, AdamW).
|
||||
|
||||
**SGD** (Stochastic Gradient Descent) — стохастический градиентный спуск:
|
||||
W_{t+1} = W_t - \eta \nabla_W L(W_t)
|
||||
Здесь \eta — шаг обучения, \nabla_W — градиент по параметрам. SGD позволяет случайно выбирать подмножество обучающих данных для каждой итерации, что ускоряет процесс и уменьшает избыточную корреляцию между примерами.
|
||||
|
||||
**Adam** (Adaptive Moment Estimation) — адаптивный алгоритм, который использует скользящую среднюю не только градиентов, но и их квадратов:
|
||||
m_t = \beta_1 m_{t-1} + (1-\beta_1) \nabla_W L(W_t)
|
||||
v_t = \beta_2 v_{t-1} + (1-\beta_2) (\nabla_W L(W_t))^2
|
||||
W_{t+1} = W_t - \eta m_t/(\sqrt{v_t}+\epsilon)
|
||||
Где \beta_1, \beta_2 — коэффициенты экспоненциального сглаживания.
|
||||
|
||||
**AdamW** — модификация Adam, в которой weight decay (имплицитная L2-регуляризация) вводится корректно, отдельно от шага градиента, что улучшает обобщающую способность моделей:
|
||||
W_{t+1} = W_t - \eta [ m_t/(\sqrt{v_t}+\epsilon) + \lambda W_t ]
|
||||
Где \lambda — коэффициент weight decay.
|
||||
|
||||
Детальное описание: https://arxiv.org/abs/1711.05101
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> optimizer = get_optimizer(model, lr=3e-4, weight_decay=0.01, optimizer_type="adamw")
|
||||
>>> for batch in dataloader:
|
||||
... loss = model(batch)
|
||||
... loss.backward()
|
||||
... optimizer.step()
|
||||
... optimizer.zero_grad()
|
||||
|
||||
"""
|
||||
import torch.optim as optim
|
||||
|
||||
|
||||
def get_optimizer(model, lr=3e-4, weight_decay=0.01, optimizer_type="adamw"):
|
||||
"""
|
||||
Возвращает оптимизатор для обучения модели.
|
||||
Фабричная функция для создания оптимизатора PyTorch по выбранному типу.
|
||||
|
||||
Параметры
|
||||
---------
|
||||
model : torch.nn.Module
|
||||
Модель, параметры которой требуется оптимизировать.
|
||||
lr : float, по умолчанию 3e-4
|
||||
Шаг обучения (learning rate).
|
||||
weight_decay : float, по умолчанию 0.01
|
||||
Коэффициент weight decay (L2-регуляризации).
|
||||
optimizer_type : str, по умолчанию 'adamw'
|
||||
Тип оптимизатора: 'adamw', 'adam' или 'sgd'.
|
||||
|
||||
Возвращаемое значение
|
||||
---------------------
|
||||
torch.optim.Optimizer
|
||||
Объект-оптимизатор, готовый к использованию.
|
||||
|
||||
Исключения
|
||||
----------
|
||||
ValueError: Если передан неизвестный тип оптимизатора.
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> optimizer = get_optimizer(model, lr=1e-3, optimizer_type='sgd')
|
||||
"""
|
||||
if optimizer_type.lower() == "adamw":
|
||||
return optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
|
||||
|
||||
@@ -1,13 +1,66 @@
|
||||
"""
|
||||
Модуль для управления динамикой шага обучения (learning rate scheduling) при обучении нейронных сетей.
|
||||
|
||||
Теоретическое обоснование:
|
||||
--------------------------
|
||||
Плавная динамика шага обучения существенно влияет на сходимость и итоговое качество моделей. Введение этапа "разогрева" (warmup) — техники, при которой шаг обучения начинается с нуля и постепенно увеличивается до целевого значения, снижает вероятность неустойчивых градиентов на старте обучения. Подобная стратегия показала свою эффективность для крупных нейронных сетей, особенно в трансформерах (Vaswani et al, 2017, https://arxiv.org/abs/1706.03762).
|
||||
|
||||
Линейный scheduler с warmup задаёт динамику learning rate по формуле:
|
||||
- если current_step < num_warmup_steps:
|
||||
lr = lr_init * (current_step / num_warmup_steps)
|
||||
- иначе:
|
||||
lr = lr_init * max(0, (num_training_steps - current_step) / (num_training_steps - num_warmup_steps))
|
||||
|
||||
Пример использования:
|
||||
---------------------
|
||||
>>> optimizer = get_optimizer(model, lr=3e-4)
|
||||
>>> scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=1000, num_training_steps=10000)
|
||||
>>> for step in range(num_training_steps):
|
||||
... optimizer.step()
|
||||
... scheduler.step()
|
||||
"""
|
||||
|
||||
from torch.optim.lr_scheduler import LambdaLR
|
||||
|
||||
def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps):
|
||||
"""
|
||||
Линейный планировщик обучения с warmup.
|
||||
Создаёт линейный планировщик изменения шага обучения (learning rate) с этапом warmup для оптимизатора PyTorch.
|
||||
|
||||
Аргументы
|
||||
---------
|
||||
optimizer : torch.optim.Optimizer
|
||||
Оптимизатор, для которого применяется scheduler.
|
||||
num_warmup_steps : int
|
||||
Количество шагов разогрева (warmup) — начиная с нулевого шага и плавного увеличения lr до номинального значения.
|
||||
num_training_steps : int
|
||||
Общее количество шагов (эпох/итераций) обучения модели.
|
||||
|
||||
Возвращаемое значение
|
||||
---------------------
|
||||
torch.optim.lr_scheduler.LambdaLR
|
||||
Планировщик lr, который следует вызывать после каждого optimizer.step() во время обучения.
|
||||
|
||||
Теоретическая справка
|
||||
---------------------
|
||||
Такой scheduler позволяет повысить стабильность и устойчивость обучения крупных моделей (особенно трансформеров), предотвращая резкие скачки градиентов в начале.
|
||||
|
||||
Пример:
|
||||
-------
|
||||
>>> optimizer = get_optimizer(model, lr=3e-4)
|
||||
>>> scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=1000, num_training_steps=10000)
|
||||
>>> for step in range(num_training_steps):
|
||||
... optimizer.step()
|
||||
... scheduler.step()
|
||||
"""
|
||||
|
||||
def lr_lambda(current_step):
|
||||
# Линейный рост lr на этапе разогрева
|
||||
if current_step < num_warmup_steps:
|
||||
return float(current_step) / float(max(1, num_warmup_steps))
|
||||
return max(0.0, float(num_training_steps - current_step) / float(max(1, num_training_steps - num_warmup_steps)))
|
||||
|
||||
# Линейное затухание lr после разогрева
|
||||
return max(
|
||||
0.0,
|
||||
float(num_training_steps - current_step)
|
||||
/ float(max(1, num_training_steps - num_warmup_steps)),
|
||||
)
|
||||
return LambdaLR(optimizer, lr_lambda)
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
"""
|
||||
Модуль для организации процесса обучения больших языковых моделей (LLM).
|
||||
|
||||
Научное и техническое обоснование
|
||||
----------------------------------
|
||||
Эффективное обучение современных трансформеров (GPT, LLaMA, Mistral и др.) опирается на принципы языкового моделирования (Language Modeling):
|
||||
- Предсказание вероятности следующего токена на основе предыдущих.
|
||||
- Использование функции потерь кросс-энтропии (cross-entropy) с маскированием паддингов.
|
||||
- Циклы обратного распространения ошибки (backpropagation), оптимизационные алгоритмы (например, AdamW), управление шагом обучения (scheduler с warmup), обрезка градиентов (grad clipping).
|
||||
|
||||
Реализация объединяет лучшие практики обучения LLM, универсальный API к моделям, датасетам, оптимизаторам и lr-схемам.
|
||||
|
||||
Подробнее: Vaswani et al. "Attention is All You Need" (2017), Radford et al. "Language Models are Unsupervised Multitask Learners" (2019)
|
||||
|
||||
Пример использования
|
||||
--------------------
|
||||
>>> trainer = Trainer(model, train_dataset, val_dataset, lr=3e-4, batch_size=8, num_epochs=3, warmup_steps=100)
|
||||
>>> trainer.train()
|
||||
"""
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch.utils.data import DataLoader
|
||||
@@ -5,15 +24,75 @@ from tqdm import tqdm
|
||||
from llm.training.optimizer import get_optimizer
|
||||
from llm.training.scheduler import get_linear_schedule_with_warmup
|
||||
|
||||
|
||||
class Trainer:
|
||||
"""
|
||||
Универсальный класс обучения LLM (GPT, LLaMA, Mistral и т.д.)
|
||||
Универсальный и расширяемый класс для обучения больших языковых моделей (Large Language Models, LLM).
|
||||
|
||||
Поддерживаются архитектуры семейства GPT, LLaMA, Mistral и другие автогрессивные модели.
|
||||
Объединяет:
|
||||
- Тренировку по задаче языкового моделирования (Causal LM)
|
||||
- Cross-entropy loss с автоматическим сдвигом логитов/меток
|
||||
- Поддержку Grad Clipping, Scheduler, Validation
|
||||
- Унифицированный даталоадер, автоматический выбор устройства (CPU/GPU)
|
||||
|
||||
Атрибуты
|
||||
--------
|
||||
model : torch.nn.Module
|
||||
Модель для обучения языковому моделированию
|
||||
train_loader : torch.utils.data.DataLoader
|
||||
Даталоадер обучающего набора
|
||||
val_loader : torch.utils.data.DataLoader или None
|
||||
Даталоадер валидационного набора (если задан)
|
||||
optimizer : torch.optim.Optimizer
|
||||
Оптимизатор параметров модели
|
||||
scheduler : torch.optim.lr_scheduler.LambdaLR
|
||||
Планировщик learning rate (инициализируется в train)
|
||||
device : torch.device
|
||||
Устройство (CPU или CUDA), куда помещается модель
|
||||
num_epochs : int
|
||||
Количество эпох обучения
|
||||
warmup_steps : int
|
||||
Число шагов warmup для scheduler
|
||||
"""
|
||||
|
||||
def __init__(self, model, train_dataset, val_dataset=None, lr=3e-4, batch_size=8, num_epochs=3, warmup_steps=100):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
train_dataset,
|
||||
val_dataset=None,
|
||||
lr=3e-4,
|
||||
batch_size=8,
|
||||
num_epochs=3,
|
||||
warmup_steps=100,
|
||||
):
|
||||
"""
|
||||
Инициализация обучающего класса Trainer.
|
||||
|
||||
Аргументы
|
||||
---------
|
||||
model : torch.nn.Module
|
||||
Модель для обучения (например, GPT, LLaMA, Mistral).
|
||||
train_dataset : torch.utils.data.Dataset
|
||||
Обучающий датасет с полями input_ids и labels.
|
||||
val_dataset : torch.utils.data.Dataset, optional
|
||||
Валидационный датасет для контроля качества обучения.
|
||||
lr : float, default=3e-4
|
||||
Начальный шаг обучения.
|
||||
batch_size : int, default=8
|
||||
Размер обучающего мини-батча.
|
||||
num_epochs : int, default=3
|
||||
Количество эпох обучения.
|
||||
warmup_steps : int, default=100
|
||||
Количество шагов разогрева (warmup) learning rate.
|
||||
"""
|
||||
self.model = model
|
||||
self.train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
|
||||
self.val_loader = DataLoader(val_dataset, batch_size=batch_size) if val_dataset else None
|
||||
self.train_loader = DataLoader(
|
||||
train_dataset, batch_size=batch_size, shuffle=True
|
||||
)
|
||||
self.val_loader = (
|
||||
DataLoader(val_dataset, batch_size=batch_size) if val_dataset else None
|
||||
)
|
||||
self.optimizer = get_optimizer(model, lr=lr)
|
||||
self.scheduler = None
|
||||
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
@@ -23,44 +102,74 @@ class Trainer:
|
||||
|
||||
def compute_lm_loss(self, logits, labels):
|
||||
"""
|
||||
Вычисляет loss для языкового моделирования.
|
||||
Сдвигает логиты и метки для предсказания следующего токена.
|
||||
Вычисляет функцию потерь (loss) для задачи автогрессивного языкового моделирования.
|
||||
|
||||
Производит сдвиг логитов и меток: предсказания делаются для следующего токена.
|
||||
Используется кросс-энтропия (CrossEntropyLoss), что соответствует максимизации логарифма правдоподобия:
|
||||
L = -log P(w_{t+1} | w_1,...,w_t)
|
||||
|
||||
Аргументы
|
||||
---------
|
||||
logits : torch.Tensor
|
||||
Логиты модели: (batch_size, seq_len, vocab_size)
|
||||
labels : torch.Tensor
|
||||
Правильные метки: (batch_size, seq_len)
|
||||
Возвращаемое значение
|
||||
---------------------
|
||||
loss : torch.Tensor
|
||||
Средний loss по batch.
|
||||
"""
|
||||
# Сдвигаем логиты и метки для языкового моделирования
|
||||
# Сдвигаем логиты и метки для языкового моделирования (автогрессия)
|
||||
shift_logits = logits[..., :-1, :].contiguous()
|
||||
shift_labels = labels[..., 1:].contiguous()
|
||||
|
||||
# Вычисляем cross-entropy loss
|
||||
|
||||
# CrossEntropyLoss (игнорируем паддинги: ignore_index=-100)
|
||||
loss = F.cross_entropy(
|
||||
shift_logits.view(-1, shift_logits.size(-1)),
|
||||
shift_labels.view(-1),
|
||||
ignore_index=-100 # Игнорируем padding tokens
|
||||
ignore_index=-100, # Padding токены не участвуют в loss
|
||||
)
|
||||
return loss
|
||||
|
||||
def train(self):
|
||||
"""
|
||||
Запускает процесс обучения модели по заданному числу эпох.
|
||||
|
||||
В процессе:
|
||||
- Применяет optimizer, scheduler с warmup и decay, grad clipping (обрезка градиентов)
|
||||
- Вызывает функцию потерь для языкового моделирования
|
||||
- Показывает динамику процесса (tqdm)
|
||||
- После каждой эпохи возможно проведение валидации
|
||||
|
||||
Параметры задаются на этапе инициализации Trainer.
|
||||
"""
|
||||
total_steps = len(self.train_loader) * self.num_epochs
|
||||
self.scheduler = get_linear_schedule_with_warmup(self.optimizer, self.warmup_steps, total_steps)
|
||||
self.scheduler = get_linear_schedule_with_warmup(
|
||||
self.optimizer, self.warmup_steps, total_steps
|
||||
)
|
||||
self.loss_history = [] # добавлено: лог средних потерь
|
||||
|
||||
for epoch in range(self.num_epochs):
|
||||
self.model.train()
|
||||
total_loss = 0
|
||||
|
||||
progress_bar = tqdm(self.train_loader, desc=f"Epoch {epoch+1}/{self.num_epochs}")
|
||||
progress_bar = tqdm(
|
||||
self.train_loader, desc=f"Epoch {epoch+1}/{self.num_epochs}"
|
||||
)
|
||||
for batch in progress_bar:
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
input_ids = batch["input_ids"].to(self.device)
|
||||
labels = batch["labels"].to(self.device)
|
||||
|
||||
# Универсально обрабатываем выход (tuple/logits)
|
||||
# Универсально обрабатываем выходы модели: tuple или просто tensor (logits)
|
||||
outputs = self.model(input_ids)
|
||||
if isinstance(outputs, tuple):
|
||||
logits = outputs[0]
|
||||
else:
|
||||
logits = outputs
|
||||
|
||||
# Trainer вычисляет loss
|
||||
|
||||
# Вычисляем loss автогрессивной LM-задачи
|
||||
loss = self.compute_lm_loss(logits, labels)
|
||||
loss.backward()
|
||||
|
||||
@@ -72,12 +181,19 @@ class Trainer:
|
||||
progress_bar.set_postfix(loss=loss.item())
|
||||
|
||||
avg_loss = total_loss / len(self.train_loader)
|
||||
self.loss_history.append(avg_loss) # добавлено: запоминаем loss
|
||||
print(f"Epoch {epoch+1} finished — avg loss: {avg_loss:.4f}")
|
||||
|
||||
if self.val_loader:
|
||||
self.evaluate()
|
||||
|
||||
def evaluate(self):
|
||||
"""
|
||||
Оценивает модель на валидационном датасете (если задан).
|
||||
|
||||
В режиме eval() модели отключается dropout и все стохастические элементы.
|
||||
Возвращает среднее значение функции потерь (loss) по всему validation set.
|
||||
"""
|
||||
self.model.eval()
|
||||
total_loss = 0
|
||||
|
||||
@@ -85,7 +201,7 @@ class Trainer:
|
||||
for batch in self.val_loader:
|
||||
input_ids = batch["input_ids"].to(self.device)
|
||||
labels = batch["labels"].to(self.device)
|
||||
|
||||
|
||||
outputs = self.model(input_ids)
|
||||
if isinstance(outputs, tuple):
|
||||
logits = outputs[0]
|
||||
@@ -95,4 +211,4 @@ class Trainer:
|
||||
total_loss += loss.item()
|
||||
|
||||
avg_loss = total_loss / len(self.val_loader)
|
||||
print(f"Validation loss: {avg_loss:.4f}")
|
||||
print(f"Validation loss: {avg_loss:.4f}")
|
||||
@@ -58,7 +58,7 @@ def gpt_config(vocab_size, embed_dim, num_heads, num_layers):
|
||||
"num_heads": num_heads,
|
||||
"num_layers": num_layers,
|
||||
"max_position_embeddings": 1024,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
}
|
||||
|
||||
|
||||
@@ -68,12 +68,14 @@ def random_inputs(batch_size, seq_len, vocab_size):
|
||||
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))
|
||||
return input_ids
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_float_inputs(batch_size, seq_len, embed_dim):
|
||||
"""Generate random floating point input tensors for testing feed forward."""
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
return inputs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_embeddings(batch_size, seq_len, embed_dim):
|
||||
"""Generate random embedding tensors for testing attention modules."""
|
||||
|
||||
65
llm/tests/core/test_cached_decoder.py
Normal file
65
llm/tests/core/test_cached_decoder.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.cached_decoder import CachedDecoder
|
||||
from llm.core.feed_forward import FeedForward
|
||||
|
||||
@pytest.fixture
|
||||
def decoder_config():
|
||||
return dict(
|
||||
num_heads=4,
|
||||
emb_size=32,
|
||||
head_size=8,
|
||||
feed_forward_layer=FeedForward(emb_size=32, dropout=0.1, activation="gelu"),
|
||||
max_seq_len=64,
|
||||
dropout=0.1
|
||||
)
|
||||
|
||||
def test_cached_decoder_init(decoder_config):
|
||||
model = CachedDecoder(**decoder_config)
|
||||
assert model is not None
|
||||
# Main attention block is usually stored as _heads or _attention (which itself includes _q _k _v)
|
||||
assert hasattr(model, '_heads') or hasattr(model, '_attention')
|
||||
assert hasattr(model, '_ff') or hasattr(model, 'feed_forward_layer')
|
||||
|
||||
def test_cached_decoder_forward_shape(decoder_config):
|
||||
model = CachedDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 3, 10, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
output, cache = model(x, use_cache=True)
|
||||
assert output.shape == (batch, seq_len, emb_size)
|
||||
assert cache is not None
|
||||
|
||||
def test_cached_decoder_forward_no_cache(decoder_config):
|
||||
model = CachedDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 2, 12, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
output, cache = model(x, use_cache=False)
|
||||
assert output.shape == (batch, seq_len, emb_size)
|
||||
assert cache is None
|
||||
|
||||
def test_cached_decoder_error_on_long_seq(decoder_config):
|
||||
model = CachedDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 1, decoder_config['max_seq_len'] + 1, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
with pytest.raises(ValueError):
|
||||
model(x)
|
||||
|
||||
def test_cached_decoder_backward(decoder_config):
|
||||
model = CachedDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 2, 7, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size, requires_grad=True)
|
||||
output, cache = model(x)
|
||||
loss = output.sum()
|
||||
loss.backward()
|
||||
assert x.grad is not None
|
||||
|
||||
def test_cached_decoder_kv_cache_chain(decoder_config):
|
||||
model = CachedDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 1, 4, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
# Первый проход — кэша нет
|
||||
_, cache = model(x, use_cache=True)
|
||||
# Второй проход — передаём кэш, добавляем еще токен:
|
||||
next_x = torch.randn(batch, 1, emb_size)
|
||||
_, cache2 = model(next_x, use_cache=True, cache=cache)
|
||||
assert cache2 is not None
|
||||
@@ -9,180 +9,233 @@ from llm.core.decoder import Decoder
|
||||
|
||||
class TestDecoder:
|
||||
"""Test cases for Decoder."""
|
||||
|
||||
|
||||
def test_initialization(self, embed_dim, num_heads):
|
||||
"""Test that Decoder can be initialized."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
assert decoder is not None
|
||||
|
||||
|
||||
# Check internal components
|
||||
assert hasattr(decoder, '_heads')
|
||||
assert hasattr(decoder, '_ff')
|
||||
assert hasattr(decoder, '_norm1')
|
||||
assert hasattr(decoder, '_norm2')
|
||||
|
||||
assert hasattr(decoder, "_heads")
|
||||
assert hasattr(decoder, "_ff")
|
||||
assert hasattr(decoder, "_norm1")
|
||||
assert hasattr(decoder, "_norm2")
|
||||
|
||||
def test_forward_pass(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test forward pass of Decoder."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
# Forward pass
|
||||
output = decoder(random_embeddings)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == random_embeddings.shape
|
||||
assert isinstance(output, torch.Tensor)
|
||||
|
||||
|
||||
def test_forward_with_causal_mask(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test forward pass with causal mask."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
batch_size, seq_len = random_embeddings.shape[:2]
|
||||
# Create causal mask
|
||||
mask = torch.tril(torch.ones(seq_len, seq_len))
|
||||
|
||||
|
||||
# Forward pass with causal mask
|
||||
output = decoder(random_embeddings, mask=mask)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == random_embeddings.shape
|
||||
|
||||
|
||||
def test_residual_connections(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test that residual connections are properly applied."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
output = decoder(random_embeddings)
|
||||
|
||||
|
||||
# With residual connections and layer norm, the output shouldn't be
|
||||
# too different from input (in terms of scale/distribution)
|
||||
input_norm = random_embeddings.norm(dim=-1).mean()
|
||||
output_norm = output.norm(dim=-1).mean()
|
||||
|
||||
|
||||
# Norms should be of similar magnitude (not exact due to transformations)
|
||||
assert 0.1 < (output_norm / input_norm) < 10.0
|
||||
|
||||
|
||||
def test_layer_norm(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test that layer normalization is applied."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
output = decoder(random_embeddings)
|
||||
|
||||
|
||||
# Check that output has reasonable statistics (due to layer norm)
|
||||
# Mean should be close to 0, std close to 1 for each sequence position
|
||||
output_mean = output.mean(dim=-1)
|
||||
output_std = output.std(dim=-1)
|
||||
|
||||
|
||||
# These are approximate checks since the data goes through multiple transformations
|
||||
assert torch.allclose(output_mean, torch.zeros_like(output_mean), atol=1.0)
|
||||
assert torch.allclose(output_std, torch.ones_like(output_std), atol=2.0)
|
||||
|
||||
|
||||
def test_gradient_flow(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test that gradients flow through Decoder."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
# Forward pass
|
||||
output = decoder(random_embeddings)
|
||||
|
||||
|
||||
# Create a dummy loss and backward pass
|
||||
loss = output.sum()
|
||||
loss.backward()
|
||||
|
||||
|
||||
# Check that gradients are computed for learnable parameters
|
||||
# in attention and feed forward components
|
||||
assert decoder._heads._layer.weight.grad is not None
|
||||
assert decoder._ff._layer1.weight.grad is not None
|
||||
assert decoder._norm1.weight.grad is not None
|
||||
assert decoder._norm2.weight.grad is not None
|
||||
|
||||
|
||||
def test_device_consistency(self, embed_dim, num_heads, random_embeddings, device):
|
||||
"""Test that Decoder works on correct device."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len).to(device)
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
).to(device)
|
||||
inputs = random_embeddings.to(device)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = decoder(inputs)
|
||||
|
||||
|
||||
# Check device consistency
|
||||
assert output.device == device
|
||||
assert decoder._heads._layer.weight.device == device
|
||||
|
||||
|
||||
def test_different_configurations(self):
|
||||
"""Test Decoder with different configurations."""
|
||||
test_cases = [
|
||||
(64, 2), # embed_dim=64, num_heads=2
|
||||
(64, 2), # embed_dim=64, num_heads=2
|
||||
(128, 4), # embed_dim=128, num_heads=4
|
||||
(256, 8), # embed_dim=256, num_heads=8
|
||||
]
|
||||
|
||||
|
||||
for embed_dim, num_heads in test_cases:
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
batch_size, seq_len = 2, 16
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
|
||||
|
||||
output = decoder(inputs)
|
||||
|
||||
|
||||
assert output.shape == inputs.shape
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size,seq_len", [(1, 8), (2, 16), (4, 32)])
|
||||
def test_different_input_shapes(self, embed_dim, num_heads, batch_size, seq_len):
|
||||
"""Test Decoder with different input shapes."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
output = decoder(inputs)
|
||||
|
||||
|
||||
assert output.shape == (batch_size, seq_len, embed_dim)
|
||||
|
||||
|
||||
def test_training_vs_evaluation(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test that Decoder behaves differently in train vs eval mode."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len, dropout=0.5)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
dropout=0.5,
|
||||
)
|
||||
|
||||
# Training mode
|
||||
decoder.train()
|
||||
output_train = decoder(random_embeddings)
|
||||
|
||||
|
||||
# Evaluation mode
|
||||
decoder.eval()
|
||||
output_eval = decoder(random_embeddings)
|
||||
|
||||
|
||||
# Outputs should be different due to dropout
|
||||
assert not torch.allclose(output_train, output_eval)
|
||||
|
||||
|
||||
def test_parameter_initialization(self, embed_dim, num_heads):
|
||||
"""Test that parameters are properly initialized."""
|
||||
head_size = embed_dim // num_heads
|
||||
max_seq_len = 1024
|
||||
decoder = Decoder(num_heads=num_heads, emb_size=embed_dim, head_size=head_size, max_seq_len=max_seq_len)
|
||||
|
||||
decoder = Decoder(
|
||||
num_heads=num_heads,
|
||||
emb_size=embed_dim,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
# Check that various components have non-zero parameters
|
||||
assert not torch.allclose(
|
||||
decoder._heads._layer.weight,
|
||||
torch.zeros_like(decoder._heads._layer.weight)
|
||||
decoder._heads._layer.weight, torch.zeros_like(decoder._heads._layer.weight)
|
||||
)
|
||||
assert not torch.allclose(
|
||||
decoder._ff._layer1.weight,
|
||||
torch.zeros_like(decoder._ff._layer1.weight)
|
||||
decoder._ff._layer1.weight, torch.zeros_like(decoder._ff._layer1.weight)
|
||||
)
|
||||
assert not torch.allclose(
|
||||
decoder._norm1.weight,
|
||||
torch.zeros_like(decoder._norm1.weight)
|
||||
decoder._norm1.weight, torch.zeros_like(decoder._norm1.weight)
|
||||
)
|
||||
|
||||
@@ -10,168 +10,178 @@ from llm.core.feed_forward import FeedForward
|
||||
|
||||
class TestFeedForward:
|
||||
"""Test cases for FeedForward."""
|
||||
|
||||
|
||||
def test_initialization(self, embed_dim):
|
||||
"""Test that FeedForward can be initialized."""
|
||||
ff = FeedForward(embed_dim)
|
||||
assert ff is not None
|
||||
|
||||
|
||||
# Check internal layers
|
||||
assert hasattr(ff, '_layer1')
|
||||
assert hasattr(ff, '_layer2')
|
||||
assert hasattr(ff, '_activation')
|
||||
assert hasattr(ff, '_dropout')
|
||||
|
||||
assert hasattr(ff, "_layer1")
|
||||
assert hasattr(ff, "_layer2")
|
||||
assert hasattr(ff, "_activation")
|
||||
assert hasattr(ff, "_dropout")
|
||||
|
||||
# Check layer dimensions
|
||||
expected_hidden_dim = embed_dim * 4 # Default expansion factor
|
||||
assert ff._layer1.weight.shape == (expected_hidden_dim, embed_dim)
|
||||
assert ff._layer2.weight.shape == (embed_dim, expected_hidden_dim)
|
||||
|
||||
|
||||
def test_forward_pass(self, embed_dim, random_float_inputs):
|
||||
"""Test forward pass of FeedForward."""
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = ff(random_float_inputs)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == random_float_inputs.shape
|
||||
assert isinstance(output, torch.Tensor)
|
||||
|
||||
|
||||
def test_custom_hidden_dim(self, embed_dim):
|
||||
"""Test FeedForward with custom hidden dimension."""
|
||||
# FeedForward doesn't support custom hidden_dim in current implementation
|
||||
# This test is not applicable
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
# Check layer dimensions (fixed 4x expansion)
|
||||
expected_hidden_dim = embed_dim * 4
|
||||
assert ff._layer1.weight.shape == (expected_hidden_dim, embed_dim)
|
||||
assert ff._layer2.weight.shape == (embed_dim, expected_hidden_dim)
|
||||
|
||||
|
||||
def test_dropout(self, embed_dim, random_float_inputs):
|
||||
"""Test that dropout is applied during training."""
|
||||
ff = FeedForward(embed_dim, dropout=0.5)
|
||||
ff.train() # Set to training mode
|
||||
|
||||
|
||||
output = ff(random_float_inputs)
|
||||
|
||||
|
||||
# In training mode with dropout, some values should be zeroed
|
||||
# This is probabilistic, so we can't assert exact zeros,
|
||||
# but we can check the structure is preserved
|
||||
assert output.shape == random_float_inputs.shape
|
||||
|
||||
|
||||
def test_no_dropout_in_eval(self, embed_dim, random_float_inputs):
|
||||
"""Test that dropout is not applied during evaluation."""
|
||||
ff = FeedForward(embed_dim, dropout=0.5)
|
||||
ff.eval() # Set to evaluation mode
|
||||
|
||||
|
||||
# Run forward pass multiple times - outputs should be identical
|
||||
output1 = ff(random_float_inputs)
|
||||
output2 = ff(random_float_inputs)
|
||||
|
||||
|
||||
assert torch.allclose(output1, output2)
|
||||
|
||||
|
||||
def test_activation_function(self, embed_dim, random_float_inputs):
|
||||
"""Test that activation function is applied."""
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
# Manually compute expected output without dropout for deterministic comparison
|
||||
hidden = ff._layer1(random_float_inputs)
|
||||
activated = ff._activation(hidden)
|
||||
expected_output = ff._layer2(activated)
|
||||
|
||||
|
||||
# Compare with forward pass in eval mode (no dropout)
|
||||
ff.eval()
|
||||
actual_output = ff(random_float_inputs)
|
||||
|
||||
|
||||
assert torch.allclose(actual_output, expected_output, rtol=1e-4)
|
||||
|
||||
|
||||
def test_gradient_flow(self, embed_dim, random_float_inputs):
|
||||
"""Test that gradients flow through FeedForward."""
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = ff(random_float_inputs)
|
||||
|
||||
|
||||
# Create a dummy loss and backward pass
|
||||
loss = output.sum()
|
||||
loss.backward()
|
||||
|
||||
|
||||
# Check that gradients are computed for learnable parameters
|
||||
assert ff._layer1.weight.grad is not None
|
||||
assert ff._layer2.weight.grad is not None
|
||||
assert not torch.allclose(ff._layer1.weight.grad,
|
||||
torch.zeros_like(ff._layer1.weight.grad))
|
||||
assert not torch.allclose(ff._layer2.weight.grad,
|
||||
torch.zeros_like(ff._layer2.weight.grad))
|
||||
|
||||
assert not torch.allclose(
|
||||
ff._layer1.weight.grad, torch.zeros_like(ff._layer1.weight.grad)
|
||||
)
|
||||
assert not torch.allclose(
|
||||
ff._layer2.weight.grad, torch.zeros_like(ff._layer2.weight.grad)
|
||||
)
|
||||
|
||||
def test_device_consistency(self, embed_dim, random_float_inputs, device):
|
||||
"""Test that FeedForward works on correct device."""
|
||||
ff = FeedForward(embed_dim).to(device)
|
||||
inputs = random_float_inputs.to(device)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = ff(inputs)
|
||||
|
||||
|
||||
# Check device consistency
|
||||
assert output.device == device
|
||||
assert ff._layer1.weight.device == device
|
||||
assert ff._layer2.weight.device == device
|
||||
|
||||
|
||||
def test_different_embed_dims(self):
|
||||
"""Test FeedForward with different embedding dimensions."""
|
||||
test_cases = [64, 128, 256, 512]
|
||||
|
||||
|
||||
for embed_dim in test_cases:
|
||||
ff = FeedForward(embed_dim)
|
||||
batch_size, seq_len = 2, 16
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
|
||||
|
||||
output = ff(inputs)
|
||||
|
||||
|
||||
assert output.shape == inputs.shape
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size,seq_len", [(1, 8), (2, 16), (4, 32)])
|
||||
def test_different_input_shapes(self, embed_dim, batch_size, seq_len):
|
||||
"""Test FeedForward with different input shapes."""
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
output = ff(inputs)
|
||||
|
||||
|
||||
assert output.shape == (batch_size, seq_len, embed_dim)
|
||||
|
||||
|
||||
def test_non_linearity(self, embed_dim, random_float_inputs):
|
||||
"""Test that FeedForward introduces non-linearity."""
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
# Create a simple linear transformation for comparison
|
||||
linear_layer = nn.Linear(embed_dim, embed_dim)
|
||||
|
||||
|
||||
# Copy weights to make comparison fair
|
||||
with torch.no_grad():
|
||||
linear_layer.weight.copy_(ff._layer2.weight @ ff._layer1.weight)
|
||||
if linear_layer.bias is not None:
|
||||
linear_layer.bias.zero_()
|
||||
|
||||
|
||||
linear_output = linear_layer(random_float_inputs)
|
||||
ff_output = ff(random_float_inputs)
|
||||
|
||||
|
||||
# FeedForward output should be different from pure linear transformation
|
||||
# due to activation function
|
||||
assert not torch.allclose(ff_output, linear_output, rtol=1e-4)
|
||||
|
||||
|
||||
def test_parameter_initialization(self, embed_dim):
|
||||
"""Test that parameters are properly initialized."""
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
# Check that weights are not all zeros
|
||||
assert not torch.allclose(ff._layer1.weight, torch.zeros_like(ff._layer1.weight))
|
||||
assert not torch.allclose(ff._layer2.weight, torch.zeros_like(ff._layer2.weight))
|
||||
|
||||
assert not torch.allclose(
|
||||
ff._layer1.weight, torch.zeros_like(ff._layer1.weight)
|
||||
)
|
||||
assert not torch.allclose(
|
||||
ff._layer2.weight, torch.zeros_like(ff._layer2.weight)
|
||||
)
|
||||
|
||||
# Check that biases are not all zeros (they should be initialized with some values)
|
||||
if ff._layer1.bias is not None:
|
||||
assert not torch.allclose(ff._layer1.bias, torch.zeros_like(ff._layer1.bias))
|
||||
assert not torch.allclose(
|
||||
ff._layer1.bias, torch.zeros_like(ff._layer1.bias)
|
||||
)
|
||||
if ff._layer2.bias is not None:
|
||||
assert not torch.allclose(ff._layer2.bias, torch.zeros_like(ff._layer2.bias))
|
||||
assert not torch.allclose(
|
||||
ff._layer2.bias, torch.zeros_like(ff._layer2.bias)
|
||||
)
|
||||
|
||||
46
llm/tests/core/test_gelu.py
Normal file
46
llm/tests/core/test_gelu.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.gelu import GELU
|
||||
|
||||
def test_gelu_shapes_and_dtype():
|
||||
gelu = GELU()
|
||||
x = torch.randn(4, 16, 8)
|
||||
y = gelu(x)
|
||||
assert y.shape == x.shape
|
||||
assert y.dtype == x.dtype
|
||||
|
||||
def test_gelu_known_values():
|
||||
gelu = GELU()
|
||||
x = torch.tensor([-3.0, 0.0, 3.0])
|
||||
y = gelu(x)
|
||||
# Сравнение с PyTorch F.gelu (которая использует точный алгоритм)
|
||||
y_ref = torch.nn.functional.gelu(x)
|
||||
diff = (y - y_ref).abs().max().item()
|
||||
assert diff < 5e-3, f"Max difference {diff} exceeds threshold"
|
||||
|
||||
def test_gelu_is_smooth_and_monotonic():
|
||||
gelu = GELU()
|
||||
x = torch.linspace(-5, 5, 100)
|
||||
y = gelu(x)
|
||||
dy = y[1:] - y[:-1]
|
||||
# Проверяем, что функция GELU хотя бы локально монотонна на большинстве промежутков
|
||||
assert (dy.mean() > 0 or dy.mean() < 0)
|
||||
|
||||
def test_gelu_gradients():
|
||||
gelu = GELU()
|
||||
x = torch.randn(3, 5, requires_grad=True)
|
||||
y = gelu(x)
|
||||
loss = y.sum()
|
||||
loss.backward()
|
||||
assert x.grad is not None
|
||||
assert x.grad.shape == x.shape
|
||||
|
||||
def test_gelu_large_vs_small():
|
||||
gelu = GELU()
|
||||
x_pos = torch.tensor([100.0])
|
||||
x_neg = torch.tensor([-100.0])
|
||||
y_pos = gelu(x_pos)
|
||||
y_neg = gelu(x_neg)
|
||||
# Для больших положительных GELU(x) ~ x, для больших отрицательных ~0
|
||||
assert torch.allclose(y_pos, x_pos, rtol=1e-4, atol=1e-4)
|
||||
assert torch.allclose(y_neg, torch.zeros_like(x_neg), rtol=1e-4, atol=1e-4)
|
||||
85
llm/tests/core/test_group_query_attention.py
Normal file
85
llm/tests/core/test_group_query_attention.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# llm/tests/core/test_group_query_attention.py
|
||||
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.group_query_attention import GroupedQueryAttention
|
||||
from llm.core.rope import RoPE
|
||||
|
||||
@pytest.fixture
|
||||
def params():
|
||||
return {
|
||||
'num_q_heads': 4,
|
||||
'num_kv_heads': 2,
|
||||
'emb_size': 16,
|
||||
'head_size': 4,
|
||||
'max_seq_len': 32,
|
||||
'window_size': 8,
|
||||
'dropout': 0.0
|
||||
}
|
||||
|
||||
def test_initialization(params):
|
||||
attn = GroupedQueryAttention(**params)
|
||||
assert isinstance(attn, GroupedQueryAttention)
|
||||
|
||||
def test_forward_shape(params):
|
||||
batch, seq = 2, 10
|
||||
x = torch.randn(batch, seq, params['emb_size'])
|
||||
attn = GroupedQueryAttention(**params)
|
||||
y, cache = attn(x)
|
||||
assert y.shape == (batch, seq, params['emb_size'])
|
||||
assert cache is not None
|
||||
assert isinstance(y, torch.Tensor)
|
||||
|
||||
def test_forward_shape_with_mask(params):
|
||||
batch, seq = 2, 10
|
||||
x = torch.randn(batch, seq, params['emb_size'])
|
||||
mask = torch.tril(torch.ones(seq, seq)).bool()
|
||||
attn = GroupedQueryAttention(**params)
|
||||
y, _ = attn(x, mask=mask)
|
||||
assert y.shape == (batch, seq, params['emb_size'])
|
||||
|
||||
def test_kv_repetition(params):
|
||||
batch, seq = 1, 3
|
||||
attn = GroupedQueryAttention(**params)
|
||||
kv = torch.randn(batch, params['num_kv_heads'], seq, params['head_size'])
|
||||
rep = attn._repeat_kv_heads(kv, params['num_q_heads'], params['num_kv_heads'])
|
||||
assert rep.shape == (batch, params['num_q_heads'], seq, params['head_size'])
|
||||
|
||||
def test_window_mask(params):
|
||||
attn = GroupedQueryAttention(**params)
|
||||
mask = attn._create_sliding_window_mask(8, 3)
|
||||
assert mask.shape == (8, 8)
|
||||
# Проверим булеву маску окна в позиции 4
|
||||
expected = torch.tensor([True, True, True, True, False, False])
|
||||
assert torch.equal(mask[4, 1:7], expected)
|
||||
|
||||
def test_forward_with_rope(params):
|
||||
batch, seq = 2, 12
|
||||
x = torch.randn(batch, seq, params['emb_size'])
|
||||
rope = RoPE(head_size=params['head_size'], max_seq_len=params['max_seq_len'])
|
||||
params2 = params.copy()
|
||||
params2['rope'] = rope
|
||||
attn = GroupedQueryAttention(**params2)
|
||||
y, _ = attn(x)
|
||||
assert y.shape == (batch, seq, params['emb_size'])
|
||||
|
||||
def test_cache_usage(params):
|
||||
batch, seq = 1, 5
|
||||
x = torch.randn(batch, seq, params['emb_size'])
|
||||
attn = GroupedQueryAttention(**params)
|
||||
# Первый проход - получаем кэш
|
||||
_, cache = attn(x)
|
||||
# Второй проход с кэшем (имитируем автокомплит seq_len=1)
|
||||
x2 = torch.randn(batch, 1, params['emb_size'])
|
||||
y2, cache2 = attn(x2, cache=cache)
|
||||
assert cache2 is not None
|
||||
assert y2.shape == (batch, 1, params['emb_size'])
|
||||
|
||||
def test_gradient_backward(params):
|
||||
batch, seq = 2, 6
|
||||
x = torch.randn(batch, seq, params['emb_size'], requires_grad=True)
|
||||
attn = GroupedQueryAttention(**params)
|
||||
y, _ = attn(x)
|
||||
y.sum().backward()
|
||||
for param in attn.parameters():
|
||||
assert param.grad is not None
|
||||
66
llm/tests/core/test_mistral_decoder.py
Normal file
66
llm/tests/core/test_mistral_decoder.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.mistral_decoder import MistralDecoder
|
||||
from llm.core.rope import RoPE
|
||||
|
||||
@pytest.fixture
|
||||
def decoder_config():
|
||||
# Current MistralDecoder is a single block (not a stack).
|
||||
return dict(
|
||||
num_q_heads=4,
|
||||
num_kv_heads=2,
|
||||
emb_size=32,
|
||||
head_size=8,
|
||||
max_seq_len=128,
|
||||
window_size=16,
|
||||
rope=RoPE(head_size=8, max_seq_len=128),
|
||||
dropout=0.0
|
||||
)
|
||||
|
||||
def test_mistral_decoder_init(decoder_config):
|
||||
model = MistralDecoder(**decoder_config)
|
||||
assert model is not None
|
||||
|
||||
def test_mistral_decoder_forward_shapes(decoder_config):
|
||||
model = MistralDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 2, 10, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
output, cache = model(x, use_cache=True)
|
||||
assert output.shape == (batch, seq_len, emb_size)
|
||||
assert cache is not None
|
||||
|
||||
def test_mistral_decoder_forward_no_cache(decoder_config):
|
||||
model = MistralDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 2, 10, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
output, cache = model(x, use_cache=False)
|
||||
assert output.shape == (batch, seq_len, emb_size)
|
||||
assert cache is None
|
||||
|
||||
def test_mistral_decoder_cache_shapes(decoder_config):
|
||||
model = MistralDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 2, 8, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
# Первый проход — без кэша
|
||||
_, cache = model(x, use_cache=True)
|
||||
# Второй проход — заполняем кэш
|
||||
x_next = torch.randn(batch, 1, emb_size)
|
||||
_, cache2 = model(x_next, use_cache=True, cache=cache)
|
||||
# Можно проверить, что кэш не None и корректной структуры:
|
||||
assert cache2 is not None
|
||||
|
||||
def test_mistral_decoder_shape_error(decoder_config):
|
||||
model = MistralDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 2, decoder_config['max_seq_len'] + 1, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size)
|
||||
with pytest.raises(ValueError):
|
||||
model(x)
|
||||
|
||||
def test_mistral_decoder_backward(decoder_config):
|
||||
model = MistralDecoder(**decoder_config)
|
||||
batch, seq_len, emb_size = 2, 10, decoder_config['emb_size']
|
||||
x = torch.randn(batch, seq_len, emb_size, requires_grad=True)
|
||||
output, _ = model(x, use_cache=False)
|
||||
loss = output.sum()
|
||||
loss.backward()
|
||||
assert x.grad is not None
|
||||
80
llm/tests/core/test_mixtral_decoder.py
Normal file
80
llm/tests/core/test_mixtral_decoder.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.mixtral_decoder import MixtralDecoder
|
||||
from llm.core.rope import RoPE
|
||||
|
||||
@pytest.fixture
|
||||
def basic_decoder():
|
||||
emb_size = 16
|
||||
num_q_heads = 4
|
||||
num_kv_heads = 2
|
||||
head_size = 4
|
||||
max_seq_len = 32
|
||||
num_experts = 4
|
||||
top_k_experts = 2
|
||||
window_size = 8
|
||||
rope = RoPE(head_size=head_size, max_seq_len=max_seq_len)
|
||||
return MixtralDecoder(
|
||||
num_q_heads=num_q_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
emb_size=emb_size,
|
||||
head_size=head_size,
|
||||
max_seq_len=max_seq_len,
|
||||
num_experts=num_experts,
|
||||
top_k_experts=top_k_experts,
|
||||
window_size=window_size,
|
||||
rope=rope,
|
||||
dropout=0.0,
|
||||
)
|
||||
|
||||
def test_forward_shape(basic_decoder):
|
||||
x = torch.randn(2, 10, 16)
|
||||
out, cache = basic_decoder(x)
|
||||
assert out.shape == (2, 10, 16)
|
||||
assert cache is None or isinstance(cache, (tuple, list))
|
||||
|
||||
def test_forward_masked(basic_decoder):
|
||||
x = torch.randn(3, 7, 16)
|
||||
mask = torch.ones(3, 7, 7, dtype=torch.bool)
|
||||
out, cache = basic_decoder(x, mask=mask)
|
||||
assert out.shape == (3, 7, 16)
|
||||
|
||||
def test_forward_with_cache_flag(basic_decoder):
|
||||
x = torch.randn(2, 8, 16)
|
||||
out, cache = basic_decoder(x, use_cache=True, cache=None)
|
||||
assert out.shape == (2, 8, 16)
|
||||
assert isinstance(cache, (tuple, list)) or cache is None
|
||||
|
||||
def test_backprop_pass(basic_decoder):
|
||||
x = torch.randn(2, 5, 16, requires_grad=True)
|
||||
out, _ = basic_decoder(x)
|
||||
y = out.sum()
|
||||
y.backward()
|
||||
assert x.grad is not None
|
||||
assert x.grad.shape == x.shape
|
||||
|
||||
def test_seq_too_long_raises(basic_decoder):
|
||||
x = torch.randn(1, 40, 16) # seq_len > max_seq_len
|
||||
with pytest.raises(Exception):
|
||||
basic_decoder(x)
|
||||
|
||||
def test_different_config():
|
||||
rope = RoPE(head_size=2, max_seq_len=12)
|
||||
decoder = MixtralDecoder(
|
||||
num_q_heads=2, num_kv_heads=2, emb_size=4, head_size=2,
|
||||
max_seq_len=12, num_experts=2, top_k_experts=1, window_size=4, rope=rope, dropout=0.1
|
||||
)
|
||||
x = torch.randn(1, 8, 4)
|
||||
out, cache = decoder(x)
|
||||
assert out.shape == x.shape
|
||||
|
||||
def test_forward_no_dropout():
|
||||
# Проверка на корректность shape при отсутствии Dropout
|
||||
rope = RoPE(head_size=2, max_seq_len=12)
|
||||
decoder = MixtralDecoder(
|
||||
num_q_heads=2, num_kv_heads=1, emb_size=4, head_size=2,
|
||||
max_seq_len=12, num_experts=2, top_k_experts=1, window_size=3, rope=rope, dropout=0.0
|
||||
)
|
||||
x = torch.randn(2, 3, 4)
|
||||
out, cache = decoder(x)
|
||||
assert out.shape == x.shape
|
||||
61
llm/tests/core/test_moe.py
Normal file
61
llm/tests/core/test_moe.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.moe import MoE
|
||||
|
||||
@pytest.fixture
|
||||
def moe():
|
||||
# Базовая MoE для коротких тестов
|
||||
return MoE(emb_size=16, num_experts=4, top_k_experts=2, dropout=0.0)
|
||||
|
||||
def test_forward_shape(moe):
|
||||
x = torch.randn(3, 5, 16) # [batch, seq, emb]
|
||||
y = moe(x)
|
||||
assert y.shape == x.shape
|
||||
|
||||
def test_forward_grad(moe):
|
||||
x = torch.randn(2, 4, 16, requires_grad=True)
|
||||
y = moe(x)
|
||||
(y.sum()).backward()
|
||||
assert x.grad is not None
|
||||
assert x.grad.shape == x.shape
|
||||
|
||||
def test_top_k_larger_than_experts():
|
||||
# top_k_experts > num_experts должно падать
|
||||
with pytest.raises(ValueError):
|
||||
MoE(emb_size=8, num_experts=2, top_k_experts=4)
|
||||
|
||||
def test_single_expert_no_error():
|
||||
# один эксперт, один топ-к — модель всё ещё валидна
|
||||
moe = MoE(emb_size=8, num_experts=1, top_k_experts=1)
|
||||
x = torch.randn(2, 2, 8)
|
||||
y = moe(x)
|
||||
assert y.shape == x.shape
|
||||
|
||||
def test_forward_trivial_weights():
|
||||
"""Проверяет, что при одинаковых весах роутера MoE возвращает усреднённое по экспертам."""
|
||||
class DummyMoE(MoE):
|
||||
def forward(self, x):
|
||||
# Роутер отдаёт всегда единичные логиты = softmax -> uniform
|
||||
self._router = torch.nn.Linear(x.size(-1), self._num_experts, bias=False)
|
||||
torch.nn.init.constant_(self._router.weight, 0.0)
|
||||
return super().forward(x)
|
||||
moe = DummyMoE(emb_size=4, num_experts=2, top_k_experts=2)
|
||||
x = torch.zeros(1, 2, 4)
|
||||
y = moe(x)
|
||||
assert y.shape == x.shape
|
||||
|
||||
def test_forward_deterministic_seed(moe):
|
||||
torch.manual_seed(42)
|
||||
x = torch.randn(2, 3, 16)
|
||||
y1 = moe(x)
|
||||
torch.manual_seed(42)
|
||||
y2 = moe(x)
|
||||
assert torch.allclose(y1, y2, atol=1e-5)
|
||||
|
||||
def test_forward_no_dropout():
|
||||
"""Без dropout MoE не меняет shape и не даёт NaN."""
|
||||
moe = MoE(emb_size=5, num_experts=3, top_k_experts=2, dropout=0.0)
|
||||
x = torch.randn(2, 7, 5)
|
||||
y = moe(x)
|
||||
assert y.shape == x.shape
|
||||
assert not torch.isnan(y).any()
|
||||
@@ -9,157 +9,183 @@ from llm.core.multi_head_attention import MultiHeadAttention
|
||||
|
||||
class TestMultiHeadAttention:
|
||||
"""Test cases for MultiHeadAttention."""
|
||||
|
||||
|
||||
def test_initialization(self, embed_dim, num_heads):
|
||||
"""Test that MultiHeadAttention can be initialized."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
assert attention is not None
|
||||
|
||||
|
||||
# Check internal attributes
|
||||
assert len(attention._heads) == num_heads
|
||||
assert attention._num_heads == num_heads
|
||||
assert attention._layer.in_features == embed_dim
|
||||
assert attention._layer.out_features == embed_dim
|
||||
|
||||
|
||||
def test_forward_pass(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test forward pass of MultiHeadAttention."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
|
||||
# Forward pass
|
||||
output, _ = attention(random_embeddings)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == random_embeddings.shape
|
||||
assert isinstance(output, torch.Tensor)
|
||||
|
||||
|
||||
def test_forward_with_mask(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test forward pass with attention mask."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
|
||||
# Create a simple mask
|
||||
seq_len = random_embeddings.shape[1]
|
||||
mask = torch.tril(torch.ones(seq_len, seq_len)) # Causal mask
|
||||
|
||||
|
||||
# Forward pass with mask
|
||||
output, _ = attention(random_embeddings, mask=mask)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == random_embeddings.shape
|
||||
|
||||
|
||||
def test_causal_mask(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test that causal mask prevents attending to future positions."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
|
||||
# Create causal mask
|
||||
seq_len = random_embeddings.shape[1]
|
||||
causal_mask = torch.tril(torch.ones(seq_len, seq_len))
|
||||
|
||||
|
||||
# Forward pass with causal mask
|
||||
output, _ = attention(random_embeddings, mask=causal_mask)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == random_embeddings.shape
|
||||
|
||||
def test_attention_weights_normalization(self, embed_dim, num_heads, random_embeddings):
|
||||
|
||||
def test_attention_weights_normalization(
|
||||
self, embed_dim, num_heads, random_embeddings
|
||||
):
|
||||
"""Test that attention weights are properly normalized."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
|
||||
# Forward pass
|
||||
output, _ = attention(random_embeddings)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == random_embeddings.shape
|
||||
|
||||
|
||||
def test_gradient_flow(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test that gradients flow through MultiHeadAttention."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
|
||||
# Forward pass
|
||||
output, _ = attention(random_embeddings)
|
||||
|
||||
|
||||
# Create a dummy loss and backward pass
|
||||
loss = output.sum()
|
||||
loss.backward()
|
||||
|
||||
|
||||
# Check that gradients are computed for learnable parameters
|
||||
assert attention._layer.weight.grad is not None
|
||||
if len(attention._heads) > 0:
|
||||
assert attention._heads[0]._q.weight.grad is not None
|
||||
|
||||
# Проверяем, что также у градиентов весов q/k/v есть значения
|
||||
assert attention._q.weight.grad is not None
|
||||
assert attention._k.weight.grad is not None
|
||||
assert attention._v.weight.grad is not None
|
||||
|
||||
def test_device_consistency(self, embed_dim, num_heads, random_embeddings, device):
|
||||
"""Test that MultiHeadAttention works on correct device."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024).to(device)
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
).to(device)
|
||||
inputs = random_embeddings.to(device)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output, _ = attention(inputs)
|
||||
|
||||
|
||||
# Check device consistency
|
||||
assert output.device == device
|
||||
assert attention._layer.weight.device == device
|
||||
|
||||
|
||||
def test_different_embed_dim_and_heads(self):
|
||||
"""Test MultiHeadAttention with different embed_dim and num_heads combinations."""
|
||||
test_cases = [
|
||||
(64, 2), # embed_dim=64, num_heads=2
|
||||
(64, 2), # embed_dim=64, num_heads=2
|
||||
(128, 4), # embed_dim=128, num_heads=4
|
||||
(256, 8), # embed_dim=256, num_heads=8
|
||||
(512, 16), # embed_dim=512, num_heads=16
|
||||
(512, 16), # embed_dim=512, num_heads=16
|
||||
]
|
||||
|
||||
|
||||
for embed_dim, num_heads in test_cases:
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
batch_size, seq_len = 2, 16
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
|
||||
|
||||
output, _ = attention(inputs)
|
||||
|
||||
|
||||
assert output.shape == inputs.shape
|
||||
|
||||
|
||||
def test_attention_output_range(self, embed_dim, num_heads, random_embeddings):
|
||||
"""Test that attention output is in reasonable range."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
|
||||
output, _ = attention(random_embeddings)
|
||||
|
||||
|
||||
# Output shouldn't have extreme values
|
||||
assert output.abs().max() < 100 # Reasonable upper bound
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size,seq_len", [(1, 8), (2, 16), (4, 32)])
|
||||
def test_different_input_shapes(self, embed_dim, num_heads, batch_size, seq_len):
|
||||
"""Test MultiHeadAttention with different input shapes."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024)
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024
|
||||
)
|
||||
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
output, _ = attention(inputs)
|
||||
|
||||
|
||||
assert output.shape == (batch_size, seq_len, embed_dim)
|
||||
|
||||
|
||||
def test_parameter_sharing(self, embed_dim, num_heads):
|
||||
"""Test that parameters are properly shared across the sequence."""
|
||||
head_size = embed_dim // num_heads
|
||||
attention = MultiHeadAttention(num_heads, embed_dim, head_size, max_seq_len=1024, dropout=0.0) # No dropout for deterministic test
|
||||
|
||||
attention = MultiHeadAttention(
|
||||
num_heads, embed_dim, head_size, max_seq_len=1024, dropout=0.0
|
||||
) # No dropout for deterministic test
|
||||
|
||||
# Create two identical sequences
|
||||
seq_len = 10
|
||||
base_sequence = torch.randn(1, seq_len, embed_dim)
|
||||
identical_sequence = base_sequence.clone()
|
||||
|
||||
|
||||
# Set to eval mode to disable dropout
|
||||
attention.eval()
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
output1, _ = attention(base_sequence)
|
||||
output2, _ = attention(identical_sequence)
|
||||
|
||||
|
||||
# With identical inputs and same parameters, outputs should be identical
|
||||
assert torch.allclose(output1, output2, rtol=1e-5)
|
||||
|
||||
@@ -10,127 +10,134 @@ from llm.core.positional_embeddings import PositionalEmbeddings
|
||||
|
||||
class TestPositionalEmbeddings:
|
||||
"""Test cases for PositionalEmbeddings."""
|
||||
|
||||
|
||||
def test_initialization(self, embed_dim):
|
||||
"""Test that PositionalEmbeddings can be initialized."""
|
||||
max_seq_len = 1024
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
assert embeddings is not None
|
||||
|
||||
|
||||
# Check that positional embeddings are created
|
||||
assert hasattr(embeddings, 'embedding')
|
||||
assert hasattr(embeddings, "embedding")
|
||||
assert embeddings.embedding.weight.shape == (max_seq_len, embed_dim)
|
||||
|
||||
|
||||
def test_forward_pass(self, embed_dim):
|
||||
"""Test forward pass of PositionalEmbeddings."""
|
||||
max_seq_len = 1024
|
||||
seq_len = 64
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
|
||||
|
||||
# Forward pass - takes sequence length, not input tensor
|
||||
output = embeddings(seq_len)
|
||||
|
||||
|
||||
# Check output shape
|
||||
expected_shape = (seq_len, embed_dim)
|
||||
assert output.shape == expected_shape
|
||||
assert isinstance(output, torch.Tensor)
|
||||
|
||||
|
||||
def test_positional_encoding_values(self, embed_dim):
|
||||
"""Test that positional encoding values are computed correctly."""
|
||||
max_seq_len = 10
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
|
||||
|
||||
# Get embeddings for all positions
|
||||
pe = embeddings(max_seq_len) # Shape: [max_seq_len, embed_dim]
|
||||
|
||||
|
||||
# Check that different positions have different embeddings
|
||||
# (since these are learnable embeddings, not fixed sine/cosine)
|
||||
for pos in range(max_seq_len):
|
||||
for i in range(pos + 1, max_seq_len):
|
||||
assert not torch.allclose(pe[pos], pe[i], rtol=1e-4)
|
||||
|
||||
|
||||
def test_different_sequence_lengths(self, embed_dim):
|
||||
"""Test PositionalEmbeddings with different sequence lengths."""
|
||||
test_cases = [
|
||||
(10, 5), # seq_len < max_seq_len
|
||||
(10, 5), # seq_len < max_seq_len
|
||||
(10, 10), # seq_len == max_seq_len
|
||||
]
|
||||
|
||||
|
||||
for max_seq_len, seq_len in test_cases:
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
|
||||
|
||||
# Get embeddings for specific sequence length
|
||||
output = embeddings(seq_len)
|
||||
|
||||
|
||||
# Output should have shape [seq_len, embed_dim]
|
||||
assert output.shape == (seq_len, embed_dim)
|
||||
|
||||
|
||||
def test_gradient_flow(self, embed_dim):
|
||||
"""Test that gradients flow through PositionalEmbeddings."""
|
||||
max_seq_len = 64
|
||||
seq_len = 32
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = embeddings(seq_len)
|
||||
|
||||
|
||||
# Create a dummy loss and backward pass
|
||||
loss = output.sum()
|
||||
loss.backward()
|
||||
|
||||
|
||||
# Positional embeddings should have gradients (they're learnable)
|
||||
assert embeddings.embedding.weight.grad is not None
|
||||
assert not torch.allclose(embeddings.embedding.weight.grad,
|
||||
torch.zeros_like(embeddings.embedding.weight.grad))
|
||||
|
||||
assert not torch.allclose(
|
||||
embeddings.embedding.weight.grad,
|
||||
torch.zeros_like(embeddings.embedding.weight.grad),
|
||||
)
|
||||
|
||||
def test_device_consistency(self, embed_dim, device):
|
||||
"""Test that PositionalEmbeddings works on correct device."""
|
||||
max_seq_len = 64
|
||||
seq_len = 32
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim).to(device)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = embeddings(seq_len)
|
||||
|
||||
|
||||
# Check device consistency
|
||||
assert output.device == device
|
||||
assert embeddings.embedding.weight.device == device
|
||||
|
||||
|
||||
def test_reproducibility(self, embed_dim):
|
||||
"""Test that positional embeddings are reproducible."""
|
||||
max_seq_len = 100
|
||||
embeddings1 = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
embeddings2 = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
|
||||
|
||||
# Different instances should have different embeddings (random initialization)
|
||||
assert not torch.allclose(embeddings1.embedding.weight, embeddings2.embedding.weight)
|
||||
|
||||
assert not torch.allclose(
|
||||
embeddings1.embedding.weight, embeddings2.embedding.weight
|
||||
)
|
||||
|
||||
# But same instance should produce same output for same input
|
||||
seq_len = 50
|
||||
output1 = embeddings1(seq_len)
|
||||
output2 = embeddings1(seq_len) # Same instance, same input
|
||||
assert torch.allclose(output1, output2)
|
||||
|
||||
|
||||
def test_positional_pattern(self, embed_dim):
|
||||
"""Test that positional embeddings create a meaningful pattern."""
|
||||
max_seq_len = 50
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
pe = embeddings(max_seq_len) # Get all positional embeddings
|
||||
|
||||
|
||||
# Check that different positions have different embeddings
|
||||
# (with high probability due to random initialization)
|
||||
assert not torch.allclose(pe[0], pe[1], rtol=1e-4)
|
||||
assert not torch.allclose(pe[10], pe[20], rtol=1e-4)
|
||||
|
||||
@pytest.mark.parametrize("max_seq_len,seq_len,embed_dim", [
|
||||
(64, 10, 64),
|
||||
(128, 50, 128),
|
||||
(256, 100, 256),
|
||||
])
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"max_seq_len,seq_len,embed_dim",
|
||||
[
|
||||
(64, 10, 64),
|
||||
(128, 50, 128),
|
||||
(256, 100, 256),
|
||||
],
|
||||
)
|
||||
def test_different_configurations(self, max_seq_len, seq_len, embed_dim):
|
||||
"""Test PositionalEmbeddings with different configurations."""
|
||||
embeddings = PositionalEmbeddings(max_seq_len, embed_dim)
|
||||
|
||||
|
||||
output = embeddings(seq_len)
|
||||
|
||||
|
||||
assert output.shape == (seq_len, embed_dim)
|
||||
|
||||
47
llm/tests/core/test_rms_norm.py
Normal file
47
llm/tests/core/test_rms_norm.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.rms_norm import RMSNorm
|
||||
|
||||
def test_rmsnorm_shape_preservation():
|
||||
norm = RMSNorm(64)
|
||||
x = torch.randn(3, 5, 64)
|
||||
y = norm(x)
|
||||
assert y.shape == x.shape
|
||||
|
||||
def test_rmsnorm_dtype_and_device():
|
||||
norm = RMSNorm(32)
|
||||
x = torch.randn(8, 32, device='cpu', dtype=torch.float64)
|
||||
y = norm(x)
|
||||
assert y.dtype == torch.float64
|
||||
assert y.device == x.device
|
||||
|
||||
def test_rmsnorm_mean_no_shift():
|
||||
norm = RMSNorm(32)
|
||||
x = torch.randn(3, 128, 32)
|
||||
y = norm(x)
|
||||
rms = torch.sqrt((y ** 2).mean(dim=-1))
|
||||
w_mean = norm._w.mean().item()
|
||||
assert torch.allclose(rms.mean(), torch.tensor(w_mean), rtol=0.2, atol=0.2)
|
||||
|
||||
def test_rmsnorm_backward():
|
||||
norm = RMSNorm(16)
|
||||
x = torch.randn(2, 15, 16, requires_grad=True)
|
||||
y = norm(x)
|
||||
loss = y.sum()
|
||||
loss.backward()
|
||||
assert x.grad is not None
|
||||
assert norm._w.grad is not None
|
||||
|
||||
def test_rmsnorm_fp16():
|
||||
norm = RMSNorm(8).half()
|
||||
x = torch.randn(2, 6, 8).half()
|
||||
y = norm(x)
|
||||
assert y.shape == x.shape
|
||||
assert y.dtype == torch.float16
|
||||
|
||||
def test_rmsnorm_large_eps_stability():
|
||||
norm = RMSNorm(16, eps=1)
|
||||
x = torch.zeros(2, 5, 16)
|
||||
y = norm(x)
|
||||
assert not torch.isnan(y).any()
|
||||
assert not torch.isinf(y).any()
|
||||
55
llm/tests/core/test_rope.py
Normal file
55
llm/tests/core/test_rope.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.rope import RoPE
|
||||
|
||||
def test_rope_shapes_and_dtype():
|
||||
rope = RoPE(head_size=8, max_seq_len=32)
|
||||
x = torch.randn(2, 4, 16, 8) # [batch, num_heads, seq_len, head_size]
|
||||
y = rope(x)
|
||||
assert y.shape == x.shape
|
||||
assert y.dtype == x.dtype
|
||||
|
||||
def test_rope_raises_on_bad_ndim():
|
||||
rope = RoPE(head_size=8, max_seq_len=16)
|
||||
x = torch.randn(2, 16, 8) # [batch, seq_len, head_size] (3D)
|
||||
with pytest.raises(AssertionError):
|
||||
_ = rope(x)
|
||||
|
||||
def test_rope_preserves_norm():
|
||||
rope = RoPE(head_size=8, max_seq_len=16)
|
||||
x = torch.randn(2, 3, 7, 8)
|
||||
x_norm = x.norm(dim=-1)
|
||||
y = rope(x)
|
||||
y_norm = y.norm(dim=-1)
|
||||
# Нормы могут немного отличаться из-за float, сравниваем с допуском
|
||||
assert torch.allclose(x_norm, y_norm, rtol=1e-5, atol=1e-7)
|
||||
|
||||
def test_rope_backward_pass():
|
||||
rope = RoPE(head_size=8, max_seq_len=16)
|
||||
x = torch.randn(2, 2, 8, 8, requires_grad=True)
|
||||
out = rope(x)
|
||||
loss = out.sum()
|
||||
loss.backward()
|
||||
assert x.grad is not None
|
||||
assert x.grad.shape == x.shape
|
||||
|
||||
@pytest.mark.parametrize("batch,num_heads,seq_len,head_size", [
|
||||
(1, 1, 4, 8),
|
||||
(2, 4, 16, 8),
|
||||
(3, 2, 7, 8),
|
||||
])
|
||||
def test_rope_various_shapes(batch, num_heads, seq_len, head_size):
|
||||
rope = RoPE(head_size=head_size, max_seq_len=32)
|
||||
x = torch.randn(batch, num_heads, seq_len, head_size)
|
||||
y = rope(x)
|
||||
assert y.shape == x.shape
|
||||
|
||||
def test_rope_start_pos():
|
||||
rope = RoPE(head_size=8, max_seq_len=32)
|
||||
x_full = torch.randn(1, 2, 8, 8)
|
||||
# Сравниваем участок результата для разных start_pos
|
||||
out1 = rope(x_full)
|
||||
out2 = rope(x_full, start_pos=2)
|
||||
assert not torch.allclose(out1, out2)
|
||||
# Для одинакового start_pos и x должны совпадать
|
||||
assert torch.allclose(rope(x_full, start_pos=1), rope(x_full, start_pos=1))
|
||||
42
llm/tests/core/test_silu.py
Normal file
42
llm/tests/core/test_silu.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.silu import SiLU
|
||||
|
||||
def test_silu_shape_and_dtype():
|
||||
silu = SiLU()
|
||||
x = torch.randn(3, 10, 8)
|
||||
y = silu(x)
|
||||
assert y.shape == x.shape
|
||||
assert y.dtype == x.dtype
|
||||
|
||||
def test_silu_known_values():
|
||||
silu = SiLU()
|
||||
x = torch.tensor([-2.0, 0.0, 2.0])
|
||||
y = silu(x)
|
||||
# PyTorch эталон
|
||||
y_ref = torch.nn.functional.silu(x)
|
||||
assert torch.allclose(y, y_ref, atol=1e-6)
|
||||
|
||||
def test_silu_large_vs_small():
|
||||
silu = SiLU()
|
||||
x_pos = torch.tensor([100.0])
|
||||
x_neg = torch.tensor([-100.0])
|
||||
y_pos = silu(x_pos)
|
||||
y_neg = silu(x_neg)
|
||||
assert torch.allclose(y_pos, x_pos, rtol=1e-4, atol=1e-4) # SiLU(x) ~ x для больших x>0
|
||||
assert torch.allclose(y_neg, torch.zeros_like(x_neg), rtol=1e-4, atol=1e-4) # SiLU(x) ~ 0 для x<0
|
||||
|
||||
def test_silu_gradients():
|
||||
silu = SiLU()
|
||||
x = torch.randn(4, 4, requires_grad=True)
|
||||
y = silu(x)
|
||||
loss = y.sum()
|
||||
loss.backward()
|
||||
assert x.grad is not None
|
||||
assert x.grad.shape == x.shape
|
||||
|
||||
def test_silu_broadcast():
|
||||
silu = SiLU()
|
||||
x = torch.randn(3, 1, 16)
|
||||
y = silu(x)
|
||||
assert y.shape == x.shape
|
||||
39
llm/tests/core/test_swi_glu.py
Normal file
39
llm/tests/core/test_swi_glu.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.core.swi_glu import SwiGLU
|
||||
|
||||
def test_swiglu_shape_and_dtype():
|
||||
swiglu = SwiGLU(emb_size=32, dropout=0.1)
|
||||
x = torch.randn(4, 10, 32)
|
||||
y = swiglu(x)
|
||||
assert y.shape == x.shape
|
||||
assert y.dtype == x.dtype
|
||||
|
||||
def test_swiglu_forward_range():
|
||||
swiglu = SwiGLU(emb_size=16, dropout=0.0)
|
||||
x = torch.randn(3, 7, 16)
|
||||
y = swiglu(x)
|
||||
assert y.abs().max() < 20
|
||||
|
||||
def test_swiglu_gradients():
|
||||
swiglu = SwiGLU(emb_size=8, dropout=0.0)
|
||||
x = torch.randn(2, 5, 8, requires_grad=True)
|
||||
out = swiglu(x)
|
||||
loss = out.pow(2).sum()
|
||||
loss.backward()
|
||||
assert x.grad is not None
|
||||
assert x.grad.shape == x.shape
|
||||
|
||||
def test_swiglu_fp16():
|
||||
swiglu = SwiGLU(emb_size=16, dropout=0.0).half()
|
||||
x = torch.randn(1, 8, 16).half()
|
||||
y = swiglu(x)
|
||||
assert y.shape == x.shape
|
||||
assert y.dtype == torch.float16
|
||||
|
||||
def test_swiglu_reproducibility():
|
||||
swiglu = SwiGLU(emb_size=8, dropout=0.0)
|
||||
x = torch.ones(2, 4, 8)
|
||||
y1 = swiglu(x)
|
||||
y2 = swiglu(x)
|
||||
assert torch.allclose(y1, y2)
|
||||
@@ -9,99 +9,103 @@ from llm.core.token_embeddings import TokenEmbeddings
|
||||
|
||||
class TestTokenEmbeddings:
|
||||
"""Test cases for TokenEmbeddings."""
|
||||
|
||||
|
||||
def test_initialization(self, vocab_size, embed_dim):
|
||||
"""Test that TokenEmbeddings can be initialized."""
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
assert embeddings is not None
|
||||
|
||||
|
||||
# Check embedding layer
|
||||
assert hasattr(embeddings, '_embedding')
|
||||
assert hasattr(embeddings, "_embedding")
|
||||
assert embeddings._embedding.weight.shape == (vocab_size, embed_dim)
|
||||
|
||||
|
||||
def test_forward_pass(self, vocab_size, embed_dim, random_inputs):
|
||||
"""Test forward pass of TokenEmbeddings."""
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = embeddings(random_inputs)
|
||||
|
||||
|
||||
# Check output shape
|
||||
assert output.shape == (random_inputs.shape[0], random_inputs.shape[1], embed_dim)
|
||||
assert output.shape == (
|
||||
random_inputs.shape[0],
|
||||
random_inputs.shape[1],
|
||||
embed_dim,
|
||||
)
|
||||
assert isinstance(output, torch.Tensor)
|
||||
|
||||
|
||||
def test_embedding_weights(self, vocab_size, embed_dim):
|
||||
"""Test that embedding weights are properly initialized."""
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
|
||||
|
||||
weights = embeddings._embedding.weight
|
||||
assert weights.requires_grad is True
|
||||
|
||||
|
||||
# Check that weights are not all zeros
|
||||
assert not torch.allclose(weights, torch.zeros_like(weights))
|
||||
|
||||
|
||||
def test_different_vocab_sizes(self):
|
||||
"""Test TokenEmbeddings with different vocabulary sizes."""
|
||||
test_cases = [
|
||||
(100, 128),
|
||||
(1000, 256),
|
||||
(50000, 512)
|
||||
]
|
||||
|
||||
test_cases = [(100, 128), (1000, 256), (50000, 512)]
|
||||
|
||||
for vocab_size, embed_dim in test_cases:
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
assert embeddings._embedding.weight.shape == (vocab_size, embed_dim)
|
||||
|
||||
|
||||
def test_gradient_flow(self, vocab_size, embed_dim, random_inputs):
|
||||
"""Test that gradients flow through TokenEmbeddings."""
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = embeddings(random_inputs)
|
||||
|
||||
|
||||
# Create a dummy loss and backward pass
|
||||
loss = output.sum()
|
||||
loss.backward()
|
||||
|
||||
|
||||
# Check that gradients are computed
|
||||
assert embeddings._embedding.weight.grad is not None
|
||||
assert not torch.allclose(embeddings._embedding.weight.grad,
|
||||
torch.zeros_like(embeddings._embedding.weight.grad))
|
||||
|
||||
assert not torch.allclose(
|
||||
embeddings._embedding.weight.grad,
|
||||
torch.zeros_like(embeddings._embedding.weight.grad),
|
||||
)
|
||||
|
||||
def test_device_consistency(self, vocab_size, embed_dim, random_inputs, device):
|
||||
"""Test that TokenEmbeddings works on correct device."""
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim).to(device)
|
||||
inputs = random_inputs.to(device)
|
||||
|
||||
|
||||
# Forward pass
|
||||
output = embeddings(inputs)
|
||||
|
||||
|
||||
# Check device consistency
|
||||
assert output.device == device
|
||||
assert embeddings._embedding.weight.device == device
|
||||
|
||||
|
||||
def test_embedding_lookup(self, vocab_size, embed_dim):
|
||||
"""Test specific embedding lookups."""
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
|
||||
|
||||
# Test lookup for specific tokens
|
||||
test_tokens = torch.tensor([[0, 1, 2], [vocab_size - 1, vocab_size - 2, vocab_size - 3]])
|
||||
|
||||
test_tokens = torch.tensor(
|
||||
[[0, 1, 2], [vocab_size - 1, vocab_size - 2, vocab_size - 3]]
|
||||
)
|
||||
|
||||
output = embeddings(test_tokens)
|
||||
|
||||
|
||||
# Check shape
|
||||
assert output.shape == (2, 3, embed_dim)
|
||||
|
||||
|
||||
# Check that different tokens have different embeddings
|
||||
# (with high probability due to random initialization)
|
||||
assert not torch.allclose(output[0, 0], output[0, 1], rtol=1e-4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size,seq_len", [(1, 1), (2, 10), (8, 64)])
|
||||
def test_different_input_shapes(self, vocab_size, embed_dim, batch_size, seq_len):
|
||||
"""Test TokenEmbeddings with different input shapes."""
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
|
||||
|
||||
inputs = torch.randint(0, vocab_size, (batch_size, seq_len))
|
||||
output = embeddings(inputs)
|
||||
|
||||
|
||||
assert output.shape == (batch_size, seq_len, embed_dim)
|
||||
|
||||
49
llm/tests/datasets/test_streaming_text_dataset.py
Normal file
49
llm/tests/datasets/test_streaming_text_dataset.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.datasets.streaming_text_dataset import StreamingTextDataset
|
||||
|
||||
class DummyTokenizer:
|
||||
def __init__(self, vocab_size=100):
|
||||
self.vocab_size = vocab_size
|
||||
def encode(self, text, **kwargs):
|
||||
return [len(w) % self.vocab_size for w in text.strip().split()]
|
||||
|
||||
def test_streaming_textdataset_basic_shape():
|
||||
texts = ["hello world", "big transformers are fun", "LLM test string"]
|
||||
tokenizer = DummyTokenizer(50)
|
||||
block_size = 7
|
||||
ds = StreamingTextDataset(texts, tokenizer, block_size)
|
||||
assert len(ds) == 3
|
||||
for i in range(len(ds)):
|
||||
item = ds[i]
|
||||
assert isinstance(item, dict)
|
||||
assert "input_ids" in item
|
||||
assert item["input_ids"].shape == (block_size,)
|
||||
assert "labels" in item
|
||||
assert item["labels"].shape == (block_size,)
|
||||
|
||||
def test_streaming_textdataset_padding_and_truncation():
|
||||
texts = ["short", "one two three four five six seven eight nine ten"]
|
||||
tokenizer = DummyTokenizer(40)
|
||||
block_size = 4
|
||||
ds = StreamingTextDataset(texts, tokenizer, block_size)
|
||||
# короткое предложение padded
|
||||
assert (ds[0]["input_ids"].shape[0] == block_size)
|
||||
# длинное предложение truncated
|
||||
assert (ds[1]["input_ids"].shape[0] == block_size)
|
||||
|
||||
def test_streaming_textdataset_index_error():
|
||||
texts = ["sample"]
|
||||
tokenizer = DummyTokenizer(10)
|
||||
ds = StreamingTextDataset(texts, tokenizer, block_size=5)
|
||||
with pytest.raises(IndexError):
|
||||
_ = ds[1]
|
||||
|
||||
def test_streaming_textdataset_content_matching():
|
||||
texts = ["foo bar baz", "abc def"]
|
||||
tokenizer = DummyTokenizer(99)
|
||||
block_size = 5
|
||||
ds = StreamingTextDataset(texts, tokenizer, block_size)
|
||||
# Проверка, что input_ids и labels совпадают точно
|
||||
for i in range(len(ds)):
|
||||
assert torch.equal(ds[i]["input_ids"], ds[i]["labels"])
|
||||
49
llm/tests/datasets/test_text_dataset.py
Normal file
49
llm/tests/datasets/test_text_dataset.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.datasets.text_dataset import TextDataset
|
||||
|
||||
class DummyTokenizer:
|
||||
def __init__(self, vocab_size=100):
|
||||
self.vocab_size = vocab_size
|
||||
def encode(self, text, **kwargs):
|
||||
return [len(w) % self.vocab_size for w in text.strip().split()]
|
||||
|
||||
def test_textdataset_shape_and_basic():
|
||||
texts = ["hello world", "this is a test", "Transformer model"]
|
||||
tokenizer = DummyTokenizer(50)
|
||||
block_size = 6
|
||||
dataset = TextDataset(texts, tokenizer, block_size=block_size)
|
||||
for i in range(len(dataset)):
|
||||
x = dataset[i]
|
||||
assert isinstance(x, dict)
|
||||
assert "input_ids" in x
|
||||
assert isinstance(x["input_ids"], torch.Tensor)
|
||||
assert x["input_ids"].shape == (block_size,)
|
||||
|
||||
def test_textdataset_truncation_and_padding():
|
||||
texts = ["one two three four five six seven", "short"]
|
||||
tokenizer = DummyTokenizer(100)
|
||||
block_size = 5
|
||||
dataset = TextDataset(texts, tokenizer, block_size=block_size)
|
||||
assert isinstance(dataset[0], dict)
|
||||
assert dataset[0]["input_ids"].shape[0] == 5
|
||||
assert dataset[1]["input_ids"].shape[0] == 5
|
||||
|
||||
def test_textdataset_index_error():
|
||||
texts = ["a", "b"]
|
||||
tokenizer = DummyTokenizer(10)
|
||||
dataset = TextDataset(texts, tokenizer, block_size=3)
|
||||
with pytest.raises(IndexError):
|
||||
_ = dataset[2]
|
||||
|
||||
def test_textdataset_encoding():
|
||||
texts = ["привет", "мир"]
|
||||
tokenizer = DummyTokenizer(20)
|
||||
block_size = 4
|
||||
dataset = TextDataset(texts, tokenizer, block_size=block_size)
|
||||
assert len(dataset) == 2
|
||||
x = dataset[0]
|
||||
assert isinstance(x, dict)
|
||||
assert "input_ids" in x
|
||||
assert isinstance(x["input_ids"], torch.Tensor)
|
||||
assert x["input_ids"].shape == (block_size,)
|
||||
62
llm/tests/datasets/test_text_with_special_tokens_dataset.py
Normal file
62
llm/tests/datasets/test_text_with_special_tokens_dataset.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.datasets.text_with_special_tokens_dataset import TextWithSpecialTokensDataset
|
||||
|
||||
class DummyTokenizer:
|
||||
def __init__(self):
|
||||
self.bos_token_id = 101
|
||||
self.eos_token_id = 102
|
||||
self.pad_token_id = 0
|
||||
def encode(self, text, add_special_tokens=False, add_bos_token=False, add_eos_token=False):
|
||||
ids = [ord(c) % 50 for c in text.strip()]
|
||||
if add_bos_token:
|
||||
ids = [self.bos_token_id] + ids
|
||||
if add_eos_token:
|
||||
ids = ids + [self.eos_token_id]
|
||||
return ids
|
||||
|
||||
def test_specialtokens_basic_bos_eos():
|
||||
texts = ["abc", "d"]
|
||||
tokenizer = DummyTokenizer()
|
||||
block_size = 6
|
||||
ds = TextWithSpecialTokensDataset(texts, tokenizer, block_size=block_size, add_bos=True, add_eos=True)
|
||||
for i in range(len(ds)):
|
||||
item = ds[i]
|
||||
assert isinstance(item, dict)
|
||||
assert "input_ids" in item
|
||||
assert item["input_ids"].shape == (block_size,)
|
||||
assert item["input_ids"][0] == tokenizer.bos_token_id
|
||||
assert item["input_ids"][item["input_ids"].ne(tokenizer.pad_token_id).sum() - 1] == tokenizer.eos_token_id
|
||||
|
||||
def test_specialtokens_padding_and_truncation():
|
||||
texts = ["qwertyuiop", "z"]
|
||||
tokenizer = DummyTokenizer()
|
||||
block_size = 5
|
||||
ds = TextWithSpecialTokensDataset(texts, tokenizer, block_size=block_size, add_bos=True)
|
||||
assert ds[0]["input_ids"].shape[0] == block_size
|
||||
assert ds[1]["input_ids"][-1] == tokenizer.pad_token_id
|
||||
|
||||
def test_specialtokens_no_bos_eos():
|
||||
texts = ["xyz"]
|
||||
tokenizer = DummyTokenizer()
|
||||
block_size = 6
|
||||
ds = TextWithSpecialTokensDataset(texts, tokenizer, block_size=block_size, add_bos=False, add_eos=False)
|
||||
item = ds[0]["input_ids"]
|
||||
assert tokenizer.bos_token_id not in item
|
||||
assert tokenizer.eos_token_id not in item
|
||||
assert item.shape == (block_size,)
|
||||
|
||||
def test_specialtokens_index_error():
|
||||
texts = ["sample"]
|
||||
tokenizer = DummyTokenizer()
|
||||
ds = TextWithSpecialTokensDataset(texts, tokenizer, block_size=8)
|
||||
with pytest.raises(IndexError):
|
||||
_ = ds[1]
|
||||
|
||||
def test_specialtokens_labels():
|
||||
texts = ["abcd"]
|
||||
tokenizer = DummyTokenizer()
|
||||
block_size = 7
|
||||
ds = TextWithSpecialTokensDataset(texts, tokenizer, block_size=block_size, add_bos=True, add_eos=True)
|
||||
item = ds[0]
|
||||
assert torch.equal(item["input_ids"], item["labels"])
|
||||
@@ -9,162 +9,160 @@ from llm.models.gpt import GPT
|
||||
|
||||
class TestGPT:
|
||||
"""Test cases for GPT model."""
|
||||
|
||||
|
||||
def test_initialization(self, gpt_config):
|
||||
"""Test that GPT can be initialized."""
|
||||
model = GPT(gpt_config)
|
||||
assert model is not None
|
||||
|
||||
|
||||
# Check that model has required components
|
||||
assert hasattr(model, '_token_embeddings')
|
||||
assert hasattr(model, '_position_embeddings')
|
||||
assert hasattr(model, '_decoders')
|
||||
assert hasattr(model, '_linear')
|
||||
assert hasattr(model, '_dropout')
|
||||
|
||||
assert hasattr(model, "_token_embeddings")
|
||||
assert hasattr(model, "_position_embeddings")
|
||||
assert hasattr(model, "_decoders")
|
||||
assert hasattr(model, "_linear")
|
||||
assert hasattr(model, "_dropout")
|
||||
|
||||
# Check number of decoder layers
|
||||
assert len(model._decoders) == gpt_config['num_layers']
|
||||
|
||||
assert len(model._decoders) == gpt_config["num_layers"]
|
||||
|
||||
def test_forward_pass(self, gpt_config, random_inputs):
|
||||
"""Test forward pass of GPT."""
|
||||
model = GPT(gpt_config)
|
||||
|
||||
|
||||
# Forward pass
|
||||
logits = model(random_inputs)
|
||||
|
||||
|
||||
# Check output shape
|
||||
batch_size, seq_len = random_inputs.shape
|
||||
vocab_size = gpt_config['vocab_size']
|
||||
vocab_size = gpt_config["vocab_size"]
|
||||
assert logits.shape == (batch_size, seq_len, vocab_size)
|
||||
assert isinstance(logits, torch.Tensor)
|
||||
|
||||
def test_forward_with_attention_mask(self, gpt_config, random_inputs, attention_mask):
|
||||
|
||||
def test_forward_with_attention_mask(
|
||||
self, gpt_config, random_inputs, attention_mask
|
||||
):
|
||||
"""Test forward pass with attention mask."""
|
||||
model = GPT(gpt_config)
|
||||
|
||||
|
||||
# Forward pass with mask
|
||||
logits = model(random_inputs, attention_mask=attention_mask)
|
||||
|
||||
|
||||
# Check output shape
|
||||
batch_size, seq_len = random_inputs.shape
|
||||
vocab_size = gpt_config['vocab_size']
|
||||
vocab_size = gpt_config["vocab_size"]
|
||||
assert logits.shape == (batch_size, seq_len, vocab_size)
|
||||
|
||||
|
||||
def test_generate_text(self, gpt_config):
|
||||
"""Test text generation."""
|
||||
model = GPT(gpt_config)
|
||||
model.eval() # Set to evaluation mode for generation
|
||||
|
||||
|
||||
# Create initial input
|
||||
batch_size = 2
|
||||
initial_seq_len = 5
|
||||
input_ids = torch.randint(0, gpt_config['vocab_size'], (batch_size, initial_seq_len))
|
||||
|
||||
input_ids = torch.randint(
|
||||
0, gpt_config["vocab_size"], (batch_size, initial_seq_len)
|
||||
)
|
||||
|
||||
# Generate text
|
||||
with torch.no_grad():
|
||||
generated = model.generate(
|
||||
x=input_ids,
|
||||
max_new_tokens=10,
|
||||
do_sample=False # Use greedy for deterministic testing
|
||||
do_sample=False, # Use greedy for deterministic testing
|
||||
)
|
||||
|
||||
|
||||
# Check output shape
|
||||
expected_seq_len = initial_seq_len + 10
|
||||
assert generated.shape == (batch_size, expected_seq_len)
|
||||
|
||||
|
||||
# Check that initial sequence is preserved
|
||||
assert torch.allclose(generated[:, :initial_seq_len], input_ids)
|
||||
|
||||
|
||||
def test_generate_with_temperature(self, gpt_config):
|
||||
"""Test text generation with temperature sampling."""
|
||||
model = GPT(gpt_config)
|
||||
model.eval()
|
||||
|
||||
|
||||
# Create initial input
|
||||
input_ids = torch.randint(0, gpt_config['vocab_size'], (1, 3))
|
||||
|
||||
input_ids = torch.randint(0, gpt_config["vocab_size"], (1, 3))
|
||||
|
||||
# Generate with temperature
|
||||
with torch.no_grad():
|
||||
generated = model.generate(
|
||||
x=input_ids,
|
||||
max_new_tokens=5,
|
||||
do_sample=True,
|
||||
temperature=0.8
|
||||
x=input_ids, max_new_tokens=5, do_sample=True, temperature=0.8
|
||||
)
|
||||
|
||||
|
||||
assert generated.shape == (1, 8) # 3 initial + 5 new tokens
|
||||
|
||||
|
||||
def test_generate_with_top_k(self, gpt_config):
|
||||
"""Test text generation with top-k sampling."""
|
||||
model = GPT(gpt_config)
|
||||
model.eval()
|
||||
|
||||
|
||||
# Create initial input
|
||||
input_ids = torch.randint(0, gpt_config['vocab_size'], (1, 3))
|
||||
|
||||
input_ids = torch.randint(0, gpt_config["vocab_size"], (1, 3))
|
||||
|
||||
# Generate with top-k
|
||||
with torch.no_grad():
|
||||
generated = model.generate(
|
||||
x=input_ids,
|
||||
max_new_tokens=5,
|
||||
do_sample=True,
|
||||
top_k=10
|
||||
x=input_ids, max_new_tokens=5, do_sample=True, top_k=10
|
||||
)
|
||||
|
||||
|
||||
assert generated.shape == (1, 8)
|
||||
|
||||
|
||||
def test_generate_with_top_p(self, gpt_config):
|
||||
"""Test text generation with top-p (nucleus) sampling."""
|
||||
model = GPT(gpt_config)
|
||||
model.eval()
|
||||
|
||||
|
||||
# Create initial input
|
||||
input_ids = torch.randint(0, gpt_config['vocab_size'], (1, 3))
|
||||
|
||||
input_ids = torch.randint(0, gpt_config["vocab_size"], (1, 3))
|
||||
|
||||
# Generate with top-p
|
||||
with torch.no_grad():
|
||||
generated = model.generate(
|
||||
x=input_ids,
|
||||
max_new_tokens=5,
|
||||
do_sample=True,
|
||||
top_p=0.9
|
||||
x=input_ids, max_new_tokens=5, do_sample=True, top_p=0.9
|
||||
)
|
||||
|
||||
|
||||
assert generated.shape == (1, 8)
|
||||
|
||||
|
||||
def test_gradient_flow(self, gpt_config, random_inputs):
|
||||
"""Test that gradients flow through GPT."""
|
||||
model = GPT(gpt_config)
|
||||
|
||||
|
||||
# Forward pass
|
||||
logits = model(random_inputs)
|
||||
|
||||
|
||||
# Create a dummy loss and backward pass
|
||||
targets = torch.randint(0, gpt_config['vocab_size'], random_inputs.shape)
|
||||
targets = torch.randint(0, gpt_config["vocab_size"], random_inputs.shape)
|
||||
loss = torch.nn.functional.cross_entropy(
|
||||
logits.view(-1, logits.size(-1)),
|
||||
targets.view(-1)
|
||||
logits.view(-1, logits.size(-1)), targets.view(-1)
|
||||
)
|
||||
loss.backward()
|
||||
|
||||
|
||||
# Check that gradients are computed for various components
|
||||
assert model._token_embeddings._embedding.weight.grad is not None
|
||||
assert model._linear.weight.grad is not None
|
||||
if len(model._decoders) > 0:
|
||||
assert model._decoders[0]._heads._heads[0]._q.weight.grad is not None
|
||||
|
||||
# Проверяем через новый интерфейс attention оптимизации:
|
||||
attn = model._decoders[0]._heads
|
||||
assert attn._q.weight.grad is not None
|
||||
assert attn._k.weight.grad is not None
|
||||
assert attn._v.weight.grad is not None
|
||||
|
||||
def test_device_consistency(self, gpt_config, random_inputs, device):
|
||||
"""Test that GPT works on correct device."""
|
||||
model = GPT(gpt_config).to(device)
|
||||
inputs = random_inputs.to(device)
|
||||
|
||||
|
||||
# Forward pass
|
||||
logits = model(inputs)
|
||||
|
||||
|
||||
# Check device consistency
|
||||
assert logits.device == device
|
||||
assert model._token_embeddings._embedding.weight.device == device
|
||||
|
||||
|
||||
def test_different_configurations(self):
|
||||
"""Test GPT with different configurations."""
|
||||
test_configs = [
|
||||
@@ -174,7 +172,7 @@ class TestGPT:
|
||||
"num_heads": 2,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 256,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
},
|
||||
{
|
||||
"vocab_size": 5000,
|
||||
@@ -182,7 +180,7 @@ class TestGPT:
|
||||
"num_heads": 4,
|
||||
"num_layers": 4,
|
||||
"max_position_embeddings": 512,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
},
|
||||
{
|
||||
"vocab_size": 10000,
|
||||
@@ -190,98 +188,94 @@ class TestGPT:
|
||||
"num_heads": 8,
|
||||
"num_layers": 6,
|
||||
"max_position_embeddings": 1024,
|
||||
"dropout": 0.1
|
||||
}
|
||||
"dropout": 0.1,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
for config in test_configs:
|
||||
model = GPT(config)
|
||||
batch_size, seq_len = 2, 16
|
||||
inputs = torch.randint(0, config['vocab_size'], (batch_size, seq_len))
|
||||
|
||||
inputs = torch.randint(0, config["vocab_size"], (batch_size, seq_len))
|
||||
|
||||
logits = model(inputs)
|
||||
|
||||
expected_shape = (batch_size, seq_len, config['vocab_size'])
|
||||
|
||||
expected_shape = (batch_size, seq_len, config["vocab_size"])
|
||||
assert logits.shape == expected_shape
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size,seq_len", [(1, 8), (2, 16), (4, 32)])
|
||||
def test_different_input_shapes(self, gpt_config, batch_size, seq_len):
|
||||
"""Test GPT with different input shapes."""
|
||||
model = GPT(gpt_config)
|
||||
|
||||
inputs = torch.randint(0, gpt_config['vocab_size'], (batch_size, seq_len))
|
||||
|
||||
inputs = torch.randint(0, gpt_config["vocab_size"], (batch_size, seq_len))
|
||||
logits = model(inputs)
|
||||
|
||||
expected_shape = (batch_size, seq_len, gpt_config['vocab_size'])
|
||||
|
||||
expected_shape = (batch_size, seq_len, gpt_config["vocab_size"])
|
||||
assert logits.shape == expected_shape
|
||||
|
||||
|
||||
def test_training_vs_evaluation(self, gpt_config, random_inputs):
|
||||
"""Test that GPT behaves differently in train vs eval mode."""
|
||||
model = GPT(gpt_config)
|
||||
|
||||
|
||||
# Training mode
|
||||
model.train()
|
||||
output_train = model(random_inputs)
|
||||
|
||||
|
||||
# Evaluation mode
|
||||
model.eval()
|
||||
output_eval = model(random_inputs)
|
||||
|
||||
|
||||
# Outputs should be different due to dropout
|
||||
assert not torch.allclose(output_train, output_eval)
|
||||
|
||||
|
||||
def test_parameter_count(self, gpt_config):
|
||||
"""Test that GPT has reasonable number of parameters."""
|
||||
model = GPT(gpt_config)
|
||||
|
||||
|
||||
total_params = sum(p.numel() for p in model.parameters())
|
||||
|
||||
|
||||
# For a small GPT model, parameters should be in reasonable range
|
||||
vocab_size = gpt_config['vocab_size']
|
||||
embed_dim = gpt_config['embed_dim']
|
||||
num_layers = gpt_config['num_layers']
|
||||
num_heads = gpt_config['num_heads']
|
||||
|
||||
vocab_size = gpt_config["vocab_size"]
|
||||
embed_dim = gpt_config["embed_dim"]
|
||||
num_layers = gpt_config["num_layers"]
|
||||
num_heads = gpt_config["num_heads"]
|
||||
|
||||
# Rough estimate: token_embeddings + output_layer + (attention + ff) * layers
|
||||
expected_min = vocab_size * embed_dim * 2 # embeddings and output
|
||||
expected_max = expected_min * 10 # Allow for decoder parameters
|
||||
|
||||
|
||||
assert expected_min < total_params < expected_max
|
||||
|
||||
|
||||
def test_causal_attention(self, gpt_config):
|
||||
"""Test that GPT uses causal attention during generation."""
|
||||
model = GPT(gpt_config)
|
||||
model.eval()
|
||||
|
||||
|
||||
# Create input with known pattern
|
||||
input_ids = torch.tensor([[1, 2, 3]]).long()
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
# Get logits for next token prediction
|
||||
logits = model(input_ids)
|
||||
|
||||
|
||||
# The model should only attend to previous tokens (causal)
|
||||
# We can't directly test attention masks in the public API,
|
||||
# but we can verify the generation works correctly
|
||||
|
||||
generated = model.generate(
|
||||
x=input_ids,
|
||||
max_new_tokens=3,
|
||||
do_sample=False
|
||||
)
|
||||
|
||||
|
||||
generated = model.generate(x=input_ids, max_new_tokens=3, do_sample=False)
|
||||
|
||||
# Generated sequence should be longer than input
|
||||
assert generated.shape[1] == input_ids.shape[1] + 3
|
||||
|
||||
|
||||
def test_output_distribution(self, gpt_config, random_inputs):
|
||||
"""Test that GPT output has proper distribution."""
|
||||
model = GPT(gpt_config)
|
||||
|
||||
|
||||
logits = model(random_inputs)
|
||||
|
||||
|
||||
# Logits should not have extreme values
|
||||
assert logits.abs().max() < 100
|
||||
|
||||
|
||||
# Softmax should produce valid probabilities
|
||||
probs = torch.softmax(logits, dim=-1)
|
||||
assert torch.allclose(probs.sum(dim=-1), torch.ones_like(probs.sum(dim=-1)))
|
||||
|
||||
53
llm/tests/models/test_gpt2.py
Normal file
53
llm/tests/models/test_gpt2.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.models.gpt.gpt2 import GPT2
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return {
|
||||
"vocab_size": 100,
|
||||
"embed_dim": 32,
|
||||
"num_heads": 4,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 16,
|
||||
"dropout": 0.0,
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def model(config):
|
||||
return GPT2(config)
|
||||
|
||||
def test_forward_basic(model):
|
||||
x = torch.randint(0, 100, (2, 8))
|
||||
logits, cache = model(x)
|
||||
assert logits.shape == (2, 8, 100)
|
||||
assert isinstance(cache, list)
|
||||
assert len(cache) == model._decoders.__len__()
|
||||
|
||||
def test_forward_with_cache(model):
|
||||
x = torch.randint(0, 100, (2, 4))
|
||||
logits, cache = model(x, use_cache=True)
|
||||
x2 = torch.randint(0, 100, (2, 1))
|
||||
logits2, cache2 = model(x2, use_cache=True, cache=cache)
|
||||
assert logits2.shape == (2, 1, 100)
|
||||
assert isinstance(cache2, list)
|
||||
|
||||
def test_generate_and_shape(model):
|
||||
x = torch.randint(0, 100, (1, 5))
|
||||
result = model.generate(x, max_new_tokens=3, do_sample=False)
|
||||
assert result.shape == (1, 8)
|
||||
|
||||
def test_forward_sequence_too_long(model, config):
|
||||
x = torch.randint(0, 100, (1, config["max_position_embeddings"] + 1))
|
||||
with pytest.raises(ValueError):
|
||||
model(x)
|
||||
|
||||
def test_generate_with_sampling_topk(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_k=5)
|
||||
assert out.shape == (1, 5)
|
||||
|
||||
def test_generate_with_sampling_topp(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_p=0.8)
|
||||
assert out.shape == (1, 5)
|
||||
53
llm/tests/models/test_llama.py
Normal file
53
llm/tests/models/test_llama.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.models.llama.llama import Llama
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return {
|
||||
"vocab_size": 100,
|
||||
"embed_dim": 32,
|
||||
"num_heads": 4,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 16,
|
||||
"dropout": 0.0,
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def model(config):
|
||||
return Llama(config)
|
||||
|
||||
def test_forward_basic(model):
|
||||
x = torch.randint(0, 100, (2, 8))
|
||||
logits, cache = model(x)
|
||||
assert logits.shape == (2, 8, 100)
|
||||
assert isinstance(cache, list)
|
||||
assert len(cache) == model._decoders.__len__()
|
||||
|
||||
def test_forward_with_cache(model):
|
||||
x = torch.randint(0, 100, (2, 4))
|
||||
logits, cache = model(x, use_cache=True)
|
||||
x2 = torch.randint(0, 100, (2, 1))
|
||||
logits2, cache2 = model(x2, use_cache=True, cache=cache)
|
||||
assert logits2.shape == (2, 1, 100)
|
||||
assert isinstance(cache2, list)
|
||||
|
||||
def test_generate_and_shape(model):
|
||||
x = torch.randint(0, 100, (1, 5))
|
||||
result = model.generate(x, max_new_tokens=3, do_sample=False)
|
||||
assert result.shape == (1, 8)
|
||||
|
||||
def test_forward_sequence_too_long(model, config):
|
||||
x = torch.randint(0, 100, (1, config["max_position_embeddings"] + 1))
|
||||
with pytest.raises(ValueError):
|
||||
model(x)
|
||||
|
||||
def test_generate_with_sampling_topk(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_k=5)
|
||||
assert out.shape == (1, 5)
|
||||
|
||||
def test_generate_with_sampling_topp(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_p=0.8)
|
||||
assert out.shape == (1, 5)
|
||||
59
llm/tests/models/test_mistral.py
Normal file
59
llm/tests/models/test_mistral.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# llm/src/llm/tests/models/test_mistral.py
|
||||
|
||||
import torch
|
||||
import pytest
|
||||
from llm.models.mistral.mistral import Mistral
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return {
|
||||
"vocab_size": 100,
|
||||
"embed_dim": 32,
|
||||
"num_q_heads": 4,
|
||||
"num_kv_heads": 2,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 16,
|
||||
"window_size": 8,
|
||||
"dropout": 0.0,
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def model(config):
|
||||
return Mistral(config)
|
||||
|
||||
def test_forward_basic(model):
|
||||
x = torch.randint(0, 100, (2, 8))
|
||||
logits, cache = model(x)
|
||||
assert logits.shape == (2, 8, 100)
|
||||
assert isinstance(cache, list)
|
||||
assert len(cache) == model._decoders.__len__()
|
||||
|
||||
def test_forward_with_cache(model):
|
||||
x = torch.randint(0, 100, (2, 4))
|
||||
logits, cache = model(x, use_cache=True)
|
||||
# Прогоняем еще раз с кэшем и 1 новым токеном
|
||||
x2 = torch.randint(0, 100, (2, 1))
|
||||
logits2, cache2 = model(x2, use_cache=True, cache=cache)
|
||||
assert logits2.shape == (2, 1, 100)
|
||||
assert isinstance(cache2, list)
|
||||
|
||||
def test_generate_and_shape(model):
|
||||
x = torch.randint(0, 100, (1, 5))
|
||||
result = model.generate(x, max_new_tokens=3, do_sample=False)
|
||||
# Вход=5, добавить 3 → итоговая длина 8
|
||||
assert result.shape == (1, 8)
|
||||
|
||||
def test_forward_sequence_too_long(model, config):
|
||||
x = torch.randint(0, 100, (1, config["max_position_embeddings"] + 1))
|
||||
with pytest.raises(ValueError):
|
||||
model(x)
|
||||
|
||||
def test_generate_with_sampling_topk(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_k=5)
|
||||
assert out.shape == (1, 5)
|
||||
|
||||
def test_generate_with_sampling_topp(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_p=0.8)
|
||||
assert out.shape == (1, 5)
|
||||
57
llm/tests/models/test_mixtral.py
Normal file
57
llm/tests/models/test_mixtral.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import torch
|
||||
import pytest
|
||||
from llm.models.mixtral.mixtral import Mixtral
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return {
|
||||
"vocab_size": 100,
|
||||
"embed_dim": 32,
|
||||
"num_q_heads": 4,
|
||||
"num_kv_heads": 2,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 16,
|
||||
"window_size": 8,
|
||||
"dropout": 0.0,
|
||||
"num_experts": 4,
|
||||
"top_k_experts": 2,
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def model(config):
|
||||
return Mixtral(config)
|
||||
|
||||
def test_forward_basic(model):
|
||||
x = torch.randint(0, 100, (2, 8))
|
||||
logits, cache = model(x)
|
||||
assert logits.shape == (2, 8, 100)
|
||||
assert isinstance(cache, list)
|
||||
assert len(cache) == model._decoders.__len__()
|
||||
|
||||
def test_forward_with_cache(model):
|
||||
x = torch.randint(0, 100, (2, 4))
|
||||
logits, cache = model(x, use_cache=True)
|
||||
x2 = torch.randint(0, 100, (2, 1))
|
||||
logits2, cache2 = model(x2, use_cache=True, cache=cache)
|
||||
assert logits2.shape == (2, 1, 100)
|
||||
assert isinstance(cache2, list)
|
||||
|
||||
def test_generate_and_shape(model):
|
||||
x = torch.randint(0, 100, (1, 5))
|
||||
result = model.generate(x, max_new_tokens=3, do_sample=False)
|
||||
assert result.shape == (1, 8)
|
||||
|
||||
def test_forward_sequence_too_long(model, config):
|
||||
x = torch.randint(0, 100, (1, config["max_position_embeddings"] + 1))
|
||||
with pytest.raises(ValueError):
|
||||
model(x)
|
||||
|
||||
def test_generate_with_sampling_topk(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_k=5)
|
||||
assert out.shape == (1, 5)
|
||||
|
||||
def test_generate_with_sampling_topp(model):
|
||||
x = torch.randint(0, 100, (1, 3))
|
||||
out = model.generate(x, max_new_tokens=2, do_sample=True, top_p=0.8)
|
||||
assert out.shape == (1, 5)
|
||||
@@ -11,25 +11,25 @@ import os
|
||||
def test_gpt_model_creation():
|
||||
"""Test that GPT model can be created and forward pass works."""
|
||||
from llm.models.gpt import GPT
|
||||
|
||||
|
||||
config = {
|
||||
"vocab_size": 1000,
|
||||
"embed_dim": 128,
|
||||
"num_heads": 4,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 256,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
}
|
||||
|
||||
|
||||
model = GPT(config)
|
||||
|
||||
|
||||
# Test forward pass
|
||||
batch_size, seq_len = 2, 16
|
||||
input_ids = torch.randint(0, config["vocab_size"], (batch_size, seq_len))
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
logits = model(input_ids)
|
||||
|
||||
|
||||
assert logits.shape == (batch_size, seq_len, config["vocab_size"])
|
||||
print("✅ GPT model creation and forward pass test passed")
|
||||
|
||||
@@ -37,27 +37,21 @@ def test_gpt_model_creation():
|
||||
def test_bpe_tokenizer_basic():
|
||||
"""Test basic BPE tokenizer functionality."""
|
||||
from llm.tokenizers import BPETokenizer
|
||||
|
||||
|
||||
tokenizer = BPETokenizer()
|
||||
|
||||
|
||||
# Train on simple texts
|
||||
texts = [
|
||||
"hello world",
|
||||
"test tokenization",
|
||||
"simple example"
|
||||
]
|
||||
|
||||
texts = ["hello world", "test tokenization", "simple example"]
|
||||
|
||||
tokenizer.train(
|
||||
texts=texts,
|
||||
vocab_size=50,
|
||||
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
texts=texts, vocab_size=50, special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
)
|
||||
|
||||
|
||||
# Test encoding/decoding
|
||||
text = "hello world"
|
||||
tokens = tokenizer.encode(text)
|
||||
decoded = tokenizer.decode(tokens)
|
||||
|
||||
|
||||
assert isinstance(tokens, list)
|
||||
assert isinstance(decoded, str)
|
||||
assert len(tokens) > 0
|
||||
@@ -67,18 +61,18 @@ def test_bpe_tokenizer_basic():
|
||||
def test_token_embeddings():
|
||||
"""Test token embeddings."""
|
||||
from llm.core.token_embeddings import TokenEmbeddings
|
||||
|
||||
|
||||
vocab_size = 1000
|
||||
embed_dim = 128
|
||||
|
||||
|
||||
embeddings = TokenEmbeddings(vocab_size, embed_dim)
|
||||
|
||||
|
||||
# Test forward pass
|
||||
batch_size, seq_len = 2, 16
|
||||
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))
|
||||
|
||||
|
||||
output = embeddings(input_ids)
|
||||
|
||||
|
||||
assert output.shape == (batch_size, seq_len, embed_dim)
|
||||
print("✅ Token embeddings test passed")
|
||||
|
||||
@@ -86,20 +80,20 @@ def test_token_embeddings():
|
||||
def test_multi_head_attention():
|
||||
"""Test multi-head attention."""
|
||||
from llm.core.multi_head_attention import MultiHeadAttention
|
||||
|
||||
|
||||
num_heads = 4
|
||||
emb_size = 128
|
||||
head_size = emb_size // num_heads
|
||||
max_seq_len = 256
|
||||
|
||||
|
||||
attention = MultiHeadAttention(num_heads, emb_size, head_size, max_seq_len)
|
||||
|
||||
|
||||
# Test forward pass
|
||||
batch_size, seq_len = 2, 16
|
||||
inputs = torch.randn(batch_size, seq_len, emb_size)
|
||||
|
||||
|
||||
output, _ = attention(inputs)
|
||||
|
||||
|
||||
assert output.shape == inputs.shape
|
||||
print("✅ Multi-head attention test passed")
|
||||
|
||||
@@ -107,17 +101,17 @@ def test_multi_head_attention():
|
||||
def test_feed_forward():
|
||||
"""Test feed forward network."""
|
||||
from llm.core.feed_forward import FeedForward
|
||||
|
||||
|
||||
embed_dim = 128
|
||||
|
||||
|
||||
ff = FeedForward(embed_dim)
|
||||
|
||||
|
||||
# Test forward pass
|
||||
batch_size, seq_len = 2, 16
|
||||
inputs = torch.randn(batch_size, seq_len, embed_dim)
|
||||
|
||||
|
||||
output = ff(inputs)
|
||||
|
||||
|
||||
assert output.shape == inputs.shape
|
||||
print("✅ Feed forward test passed")
|
||||
|
||||
@@ -125,29 +119,25 @@ def test_feed_forward():
|
||||
def test_gpt_generation():
|
||||
"""Test GPT text generation."""
|
||||
from llm.models.gpt import GPT
|
||||
|
||||
|
||||
config = {
|
||||
"vocab_size": 1000,
|
||||
"embed_dim": 128,
|
||||
"num_heads": 4,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 256,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
}
|
||||
|
||||
|
||||
model = GPT(config)
|
||||
model.eval()
|
||||
|
||||
|
||||
# Test greedy generation
|
||||
input_ids = torch.randint(0, config["vocab_size"], (1, 5))
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
generated = model.generate(
|
||||
x=input_ids,
|
||||
max_new_tokens=3,
|
||||
do_sample=False
|
||||
)
|
||||
|
||||
generated = model.generate(x=input_ids, max_new_tokens=3, do_sample=False)
|
||||
|
||||
assert generated.shape == (1, 8) # 5 initial + 3 new tokens
|
||||
print("✅ GPT generation test passed")
|
||||
|
||||
@@ -155,50 +145,48 @@ def test_gpt_generation():
|
||||
def test_bpe_tokenizer_save_load():
|
||||
"""Test BPE tokenizer save/load functionality."""
|
||||
from llm.tokenizers import BPETokenizer
|
||||
|
||||
|
||||
tokenizer = BPETokenizer()
|
||||
|
||||
|
||||
# Train on simple texts
|
||||
texts = ["hello world", "test save load"]
|
||||
tokenizer.train(
|
||||
texts=texts,
|
||||
vocab_size=30,
|
||||
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
texts=texts, vocab_size=30, special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
)
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
save_path = os.path.join(temp_dir, "test_tokenizer.json")
|
||||
|
||||
|
||||
# Save tokenizer
|
||||
tokenizer.save(save_path)
|
||||
assert os.path.exists(save_path)
|
||||
|
||||
|
||||
# Load tokenizer
|
||||
loaded_tokenizer = BPETokenizer.load(save_path)
|
||||
|
||||
|
||||
# Test that vocab size is the same
|
||||
assert tokenizer.get_vocab_size() == loaded_tokenizer.get_vocab_size()
|
||||
|
||||
|
||||
# Test that vocabularies are the same
|
||||
assert tokenizer.get_vocab() == loaded_tokenizer.get_vocab()
|
||||
|
||||
|
||||
# Test that both can encode/decode (even if tokens differ due to BPE state)
|
||||
text = "hello world"
|
||||
original_tokens = tokenizer.encode(text)
|
||||
loaded_tokens = loaded_tokenizer.encode(text)
|
||||
|
||||
|
||||
# Both should produce valid token lists
|
||||
assert isinstance(original_tokens, list)
|
||||
assert isinstance(loaded_tokens, list)
|
||||
assert len(original_tokens) > 0
|
||||
assert len(loaded_tokens) > 0
|
||||
|
||||
|
||||
# Both should be able to decode
|
||||
original_decoded = tokenizer.decode(original_tokens)
|
||||
loaded_decoded = loaded_tokenizer.decode(loaded_tokens)
|
||||
assert isinstance(original_decoded, str)
|
||||
assert isinstance(loaded_decoded, str)
|
||||
|
||||
|
||||
print("✅ BPE tokenizer save/load test passed")
|
||||
|
||||
|
||||
@@ -206,18 +194,16 @@ def test_gpt_with_tokenizer():
|
||||
"""Test GPT model with tokenizer integration."""
|
||||
from llm.models.gpt import GPT
|
||||
from llm.tokenizers import BPETokenizer
|
||||
|
||||
|
||||
# Create and train tokenizer
|
||||
tokenizer = BPETokenizer()
|
||||
texts = ["hello world", "test integration"]
|
||||
tokenizer.train(
|
||||
texts=texts,
|
||||
vocab_size=50,
|
||||
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
texts=texts, vocab_size=50, special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
)
|
||||
|
||||
|
||||
vocab_size = tokenizer.get_vocab_size()
|
||||
|
||||
|
||||
# Create GPT model with tokenizer's vocab size
|
||||
config = {
|
||||
"vocab_size": vocab_size,
|
||||
@@ -225,19 +211,19 @@ def test_gpt_with_tokenizer():
|
||||
"num_heads": 4,
|
||||
"num_layers": 2,
|
||||
"max_position_embeddings": 256,
|
||||
"dropout": 0.1
|
||||
"dropout": 0.1,
|
||||
}
|
||||
|
||||
|
||||
model = GPT(config)
|
||||
|
||||
|
||||
# Test with tokenized input
|
||||
text = "hello world"
|
||||
tokens = tokenizer.encode(text, add_special_tokens=False)
|
||||
input_ids = torch.tensor([tokens])
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
logits = model(input_ids)
|
||||
|
||||
|
||||
assert logits.shape == (1, len(tokens), vocab_size)
|
||||
print("✅ GPT with tokenizer integration test passed")
|
||||
|
||||
@@ -245,7 +231,7 @@ def test_gpt_with_tokenizer():
|
||||
def run_all_tests():
|
||||
"""Run all basic tests."""
|
||||
print("🧪 Running basic tests for llm library...")
|
||||
|
||||
|
||||
test_gpt_model_creation()
|
||||
test_bpe_tokenizer_basic()
|
||||
test_token_embeddings()
|
||||
@@ -254,7 +240,7 @@ def run_all_tests():
|
||||
test_gpt_generation()
|
||||
test_bpe_tokenizer_save_load()
|
||||
test_gpt_with_tokenizer()
|
||||
|
||||
|
||||
print("🎉 All basic tests passed!")
|
||||
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ from llm.tokenizers import BaseTokenizer
|
||||
|
||||
class ConcreteTokenizer(BaseTokenizer):
|
||||
"""Concrete implementation for testing BaseTokenizer."""
|
||||
|
||||
|
||||
def train(self, texts: list, vocab_size: int = 1000, **kwargs):
|
||||
"""Dummy implementation for testing."""
|
||||
pass
|
||||
|
||||
|
||||
def encode(self, text: str, **kwargs) -> list:
|
||||
"""Dummy implementation for testing."""
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
def decode(self, tokens: list, **kwargs) -> str:
|
||||
"""Dummy implementation for testing."""
|
||||
return "decoded text"
|
||||
@@ -24,33 +24,33 @@ class ConcreteTokenizer(BaseTokenizer):
|
||||
|
||||
class TestBaseTokenizer:
|
||||
"""Test cases for BaseTokenizer."""
|
||||
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that BaseTokenizer can be initialized through concrete class."""
|
||||
tokenizer = ConcreteTokenizer()
|
||||
assert tokenizer is not None
|
||||
assert tokenizer.vocab == {}
|
||||
assert tokenizer.vocab_size == 0
|
||||
|
||||
|
||||
def test_encode_implemented(self):
|
||||
"""Test that encode method works in concrete implementation."""
|
||||
tokenizer = ConcreteTokenizer()
|
||||
result = tokenizer.encode("test text")
|
||||
assert result == [1, 2, 3]
|
||||
|
||||
|
||||
def test_decode_implemented(self):
|
||||
"""Test that decode method works in concrete implementation."""
|
||||
tokenizer = ConcreteTokenizer()
|
||||
result = tokenizer.decode([1, 2, 3])
|
||||
assert result == "decoded text"
|
||||
|
||||
|
||||
def test_get_vocab_size(self):
|
||||
"""Test that get_vocab_size method works."""
|
||||
tokenizer = ConcreteTokenizer()
|
||||
tokenizer.vocab = {"a": 0, "b": 1, "c": 2}
|
||||
tokenizer.vocab_size = 3
|
||||
assert tokenizer.get_vocab_size() == 3
|
||||
|
||||
|
||||
def test_get_vocab(self):
|
||||
"""Test that get_vocab method works."""
|
||||
tokenizer = ConcreteTokenizer()
|
||||
|
||||
@@ -10,18 +10,18 @@ from llm.tokenizers import BPETokenizer
|
||||
|
||||
class TestBPETokenizer:
|
||||
"""Test cases for BPETokenizer."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_texts(self):
|
||||
"""Sample texts for training tokenizer."""
|
||||
return [
|
||||
"Искусственный интеллект",
|
||||
"Нейронные сети",
|
||||
"Нейронные сети",
|
||||
"Машинное обучение",
|
||||
"Глубокое обучение",
|
||||
"Трансформеры"
|
||||
"Трансформеры",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def trained_tokenizer(self, sample_texts):
|
||||
"""Create and train a BPE tokenizer."""
|
||||
@@ -29,128 +29,130 @@ class TestBPETokenizer:
|
||||
tokenizer.train(
|
||||
texts=sample_texts,
|
||||
vocab_size=100,
|
||||
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
)
|
||||
return tokenizer
|
||||
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that BPETokenizer can be initialized."""
|
||||
tokenizer = BPETokenizer()
|
||||
assert tokenizer is not None
|
||||
|
||||
|
||||
def test_train_tokenizer(self, sample_texts):
|
||||
"""Test that tokenizer can be trained."""
|
||||
tokenizer = BPETokenizer()
|
||||
tokenizer.train(
|
||||
texts=sample_texts,
|
||||
vocab_size=50,
|
||||
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"],
|
||||
)
|
||||
|
||||
|
||||
assert tokenizer.get_vocab_size() > 0
|
||||
assert len(tokenizer.get_vocab()) == tokenizer.get_vocab_size()
|
||||
|
||||
|
||||
def test_encode_decode(self, trained_tokenizer):
|
||||
"""Test encoding and decoding text."""
|
||||
text = "Искусственный интеллект"
|
||||
|
||||
|
||||
# Encode text
|
||||
tokens = trained_tokenizer.encode(text)
|
||||
assert isinstance(tokens, list)
|
||||
assert len(tokens) > 0
|
||||
assert all(isinstance(token, int) for token in tokens)
|
||||
|
||||
|
||||
# Decode tokens
|
||||
decoded_text = trained_tokenizer.decode(tokens)
|
||||
assert isinstance(decoded_text, str)
|
||||
# Decoded text should be similar to original (may have special tokens)
|
||||
assert len(decoded_text) > 0
|
||||
|
||||
|
||||
def test_encode_with_special_tokens(self, trained_tokenizer):
|
||||
"""Test encoding with special tokens."""
|
||||
text = "Нейронные сети"
|
||||
|
||||
|
||||
# Without special tokens
|
||||
tokens_no_special = trained_tokenizer.encode(text, add_special_tokens=False)
|
||||
|
||||
|
||||
# With special tokens
|
||||
tokens_with_special = trained_tokenizer.encode(text, add_special_tokens=True)
|
||||
|
||||
|
||||
# Should have more tokens when special tokens are added
|
||||
assert len(tokens_with_special) >= len(tokens_no_special)
|
||||
|
||||
|
||||
def test_vocab_size(self, trained_tokenizer):
|
||||
"""Test vocabulary size."""
|
||||
vocab_size = trained_tokenizer.get_vocab_size()
|
||||
assert isinstance(vocab_size, int)
|
||||
assert vocab_size > 0
|
||||
|
||||
|
||||
vocab = trained_tokenizer.get_vocab()
|
||||
assert isinstance(vocab, dict)
|
||||
assert len(vocab) == vocab_size
|
||||
|
||||
|
||||
def test_special_tokens(self, trained_tokenizer):
|
||||
"""Test that special tokens are in vocabulary."""
|
||||
vocab = trained_tokenizer.get_vocab()
|
||||
|
||||
|
||||
# Check that special tokens are in vocabulary
|
||||
special_tokens = ["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||
for token in special_tokens:
|
||||
assert token in vocab
|
||||
assert isinstance(vocab[token], int)
|
||||
|
||||
|
||||
def test_save_load(self, trained_tokenizer, sample_texts):
|
||||
"""Test saving and loading tokenizer."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
save_path = os.path.join(temp_dir, "test_tokenizer.json")
|
||||
|
||||
|
||||
# Save tokenizer
|
||||
trained_tokenizer.save(save_path)
|
||||
assert os.path.exists(save_path)
|
||||
|
||||
|
||||
# Load tokenizer
|
||||
loaded_tokenizer = BPETokenizer.load(save_path)
|
||||
assert loaded_tokenizer is not None
|
||||
|
||||
|
||||
# Check that loaded tokenizer works the same
|
||||
original_vocab = trained_tokenizer.get_vocab()
|
||||
loaded_vocab = loaded_tokenizer.get_vocab()
|
||||
|
||||
|
||||
assert original_vocab == loaded_vocab
|
||||
assert trained_tokenizer.get_vocab_size() == loaded_tokenizer.get_vocab_size()
|
||||
|
||||
assert (
|
||||
trained_tokenizer.get_vocab_size() == loaded_tokenizer.get_vocab_size()
|
||||
)
|
||||
|
||||
# Test encoding consistency
|
||||
text = sample_texts[0]
|
||||
original_tokens = trained_tokenizer.encode(text)
|
||||
loaded_tokens = loaded_tokenizer.encode(text)
|
||||
|
||||
|
||||
assert original_tokens == loaded_tokens
|
||||
|
||||
|
||||
def test_unknown_tokens(self, trained_tokenizer):
|
||||
"""Test handling of unknown tokens."""
|
||||
# Use text that likely contains unknown subwords
|
||||
text = "xyzabc123" # Random text that shouldn't be in training data
|
||||
|
||||
|
||||
tokens = trained_tokenizer.encode(text)
|
||||
assert len(tokens) > 0
|
||||
|
||||
|
||||
# Should be able to decode back (even if it's mostly unk tokens)
|
||||
decoded = trained_tokenizer.decode(tokens)
|
||||
assert isinstance(decoded, str)
|
||||
|
||||
|
||||
def test_empty_text(self, trained_tokenizer):
|
||||
"""Test encoding and decoding empty text."""
|
||||
tokens = trained_tokenizer.encode("")
|
||||
assert isinstance(tokens, list)
|
||||
|
||||
|
||||
decoded = trained_tokenizer.decode([])
|
||||
assert decoded == ""
|
||||
|
||||
|
||||
def test_tokenize_method(self, trained_tokenizer):
|
||||
"""Test the tokenize method."""
|
||||
text = "Искусственный интеллект"
|
||||
tokens = trained_tokenizer.tokenize(text)
|
||||
|
||||
|
||||
assert isinstance(tokens, list)
|
||||
assert len(tokens) > 0
|
||||
assert all(isinstance(token, str) for token in tokens)
|
||||
|
||||
35
llm/tests/training/test_optimizer.py
Normal file
35
llm/tests/training/test_optimizer.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
import torch.nn as nn
|
||||
from llm.training.optimizer import get_optimizer
|
||||
|
||||
class DummyModel(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.linear = nn.Linear(10, 1)
|
||||
|
||||
def test_get_optimizer_adamw():
|
||||
model = DummyModel()
|
||||
optimizer = get_optimizer(model, lr=1e-3, weight_decay=0.02, optimizer_type="adamw")
|
||||
assert optimizer.__class__.__name__ == 'AdamW'
|
||||
assert optimizer.defaults['lr'] == 1e-3
|
||||
assert optimizer.defaults['weight_decay'] == 0.02
|
||||
|
||||
def test_get_optimizer_adam():
|
||||
model = DummyModel()
|
||||
optimizer = get_optimizer(model, lr=1e-4, weight_decay=0.01, optimizer_type="adam")
|
||||
assert optimizer.__class__.__name__ == 'Adam'
|
||||
assert optimizer.defaults['lr'] == 1e-4
|
||||
assert optimizer.defaults['weight_decay'] == 0.01
|
||||
|
||||
def test_get_optimizer_sgd():
|
||||
model = DummyModel()
|
||||
optimizer = get_optimizer(model, lr=0.1, optimizer_type="sgd")
|
||||
assert optimizer.__class__.__name__ == 'SGD'
|
||||
assert optimizer.defaults['lr'] == 0.1
|
||||
# SGD: weight_decay по умолчанию 0 для этого вызова
|
||||
assert optimizer.defaults['momentum'] == 0.9
|
||||
|
||||
def test_get_optimizer_invalid():
|
||||
model = DummyModel()
|
||||
with pytest.raises(ValueError):
|
||||
get_optimizer(model, optimizer_type="nonexistent")
|
||||
62
llm/tests/training/test_scheduler.py
Normal file
62
llm/tests/training/test_scheduler.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from llm.training.scheduler import get_linear_schedule_with_warmup
|
||||
from llm.training.optimizer import get_optimizer
|
||||
|
||||
class DummyModel(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.linear = nn.Linear(2, 2)
|
||||
|
||||
def test_scheduler_warmup_and_decay():
|
||||
model = DummyModel()
|
||||
base_lr = 0.1
|
||||
warmup_steps = 5
|
||||
total_steps = 20
|
||||
optimizer = get_optimizer(model, lr=base_lr, optimizer_type="sgd")
|
||||
scheduler = get_linear_schedule_with_warmup(
|
||||
optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps)
|
||||
|
||||
lrs = [optimizer.param_groups[0]['lr']] # lr до первого .step()
|
||||
for _ in range(total_steps):
|
||||
optimizer.step()
|
||||
scheduler.step()
|
||||
lrs.append(optimizer.param_groups[0]['lr'])
|
||||
|
||||
# Проверяем warmup: lr должен расти линейно в первых warmup_steps (начиная с шага 1)
|
||||
for i in range(warmup_steps + 1):
|
||||
expected = base_lr * min(i, warmup_steps) / max(1, warmup_steps)
|
||||
assert abs(lrs[i] - expected) < 1e-6, f"Warmup step {i}: lr={lrs[i]}, expected={expected}"
|
||||
# Проверяем decay: после warmup lr затухает
|
||||
for i in range(warmup_steps + 1, total_steps + 1):
|
||||
expected = base_lr * max(0.0, (total_steps - (i - 0)) / max(1, total_steps - warmup_steps))
|
||||
assert abs(lrs[i] - expected) < 1e-6, f"Decay step {i}: lr={lrs[i]}, expected={expected}"
|
||||
assert lrs[-1] == 0.0
|
||||
|
||||
def test_scheduler_no_warmup():
|
||||
model = DummyModel()
|
||||
base_lr = 0.1
|
||||
warmup_steps = 0
|
||||
total_steps = 10
|
||||
optimizer = get_optimizer(model, lr=base_lr, optimizer_type="adam")
|
||||
scheduler = get_linear_schedule_with_warmup(
|
||||
optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps)
|
||||
lrs = [optimizer.param_groups[0]['lr']]
|
||||
for _ in range(total_steps):
|
||||
optimizer.step()
|
||||
scheduler.step()
|
||||
lrs.append(optimizer.param_groups[0]['lr'])
|
||||
|
||||
for i in range(total_steps + 1):
|
||||
expected = base_lr * max(0.0, (total_steps - i) / max(1, total_steps - warmup_steps))
|
||||
assert abs(lrs[i] - expected) < 1e-6, f"Step {i}: lr={lrs[i]}, expected={expected}"
|
||||
assert lrs[-1] == 0.0
|
||||
|
||||
def test_scheduler_full_decay_to_zero():
|
||||
model = DummyModel()
|
||||
optimizer = get_optimizer(model, lr=1.0, optimizer_type="adamw")
|
||||
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=2, num_training_steps=2)
|
||||
scheduler.step()
|
||||
scheduler.step()
|
||||
for param_group in optimizer.param_groups:
|
||||
assert param_group['lr'] == 0.0
|
||||
62
llm/tests/training/test_trainer.py
Normal file
62
llm/tests/training/test_trainer.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import Dataset
|
||||
from llm.training.trainer import Trainer
|
||||
|
||||
# Синтетический небольшой датасет для автогрессивной LM задачи
|
||||
class ToyLMDataset(Dataset):
|
||||
def __init__(self, num_samples=16, seq_len=8, vocab_size=16):
|
||||
self.data = torch.randint(1, vocab_size, (num_samples, seq_len))
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
def __getitem__(self, idx):
|
||||
# labels == input_ids (identity task)
|
||||
return {"input_ids": self.data[idx], "labels": self.data[idx]}
|
||||
|
||||
# Простая dummy-модель — 1 слой linear over vocab
|
||||
class TinyModel(nn.Module):
|
||||
def __init__(self, vocab_size=16, seq_len=8):
|
||||
super().__init__()
|
||||
self.linear = nn.Linear(seq_len, vocab_size)
|
||||
def forward(self, x):
|
||||
# logits: (batch, seq_len, vocab_size)
|
||||
# Для простоты делаем транспонирование
|
||||
return self.linear(x.float()).unsqueeze(1).expand(-1, x.shape[1], -1)
|
||||
|
||||
def test_train_runs_without_errors():
|
||||
train_data = ToyLMDataset(num_samples=16, seq_len=8, vocab_size=16)
|
||||
model = TinyModel(vocab_size=16, seq_len=8)
|
||||
trainer = Trainer(model, train_data, lr=1e-3, batch_size=4, num_epochs=1, warmup_steps=2)
|
||||
trainer.train()
|
||||
|
||||
def test_trainer_evaluate_runs():
|
||||
train_data = ToyLMDataset(num_samples=8)
|
||||
val_data = ToyLMDataset(num_samples=8)
|
||||
model = TinyModel()
|
||||
trainer = Trainer(model, train_data, val_data, lr=1e-3, batch_size=4, num_epochs=1, warmup_steps=2)
|
||||
trainer.train()
|
||||
trainer.evaluate()
|
||||
|
||||
def test_trainer_tuple_output():
|
||||
# Модель, возвращающая кортеж (logits, extra)
|
||||
class TupleModel(nn.Module):
|
||||
def __init__(self, vocab_size=16, seq_len=8):
|
||||
super().__init__()
|
||||
self.linear = nn.Linear(seq_len, vocab_size)
|
||||
def forward(self, x):
|
||||
logits = self.linear(x.float()).unsqueeze(1).expand(-1, x.shape[1], -1)
|
||||
extra = torch.zeros(1)
|
||||
return logits, extra
|
||||
|
||||
train_data = ToyLMDataset(num_samples=8)
|
||||
model = TupleModel()
|
||||
trainer = Trainer(model, train_data, lr=1e-3, batch_size=2, num_epochs=1, warmup_steps=1)
|
||||
trainer.train()
|
||||
|
||||
def test_trainer_loss_decreases():
|
||||
train_data = ToyLMDataset(num_samples=32, seq_len=8, vocab_size=8)
|
||||
model = TinyModel(vocab_size=8, seq_len=8)
|
||||
trainer = Trainer(model, train_data, lr=0.05, batch_size=8, num_epochs=2, warmup_steps=1)
|
||||
trainer.train()
|
||||
avg_losses = trainer.loss_history
|
||||
assert avg_losses[-1] <= avg_losses[0] or abs(avg_losses[-1] - avg_losses[0]) < 1e-3
|
||||
1638
notebooks/llama.ipynb
Normal file
1638
notebooks/llama.ipynb
Normal file
File diff suppressed because one or more lines are too long
3267
notebooks/mistral.ipynb
Normal file
3267
notebooks/mistral.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
1510
notebooks/mixstral.ipynb
Normal file
1510
notebooks/mixstral.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@ dependencies = [
|
||||
"llm",
|
||||
"tqdm>=4,<5",
|
||||
"ipykernel",
|
||||
"torch==2.8.0",
|
||||
"matplotlib==3.10.6",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
521
uv.lock
generated
521
uv.lock
generated
@@ -525,6 +525,163 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "contourpy"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "contourpy"
|
||||
version = "1.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version >= '3.12' and python_full_version < '3.14'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.10.7"
|
||||
@@ -629,6 +786,15 @@ toml = [
|
||||
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "datasets"
|
||||
version = "4.1.1"
|
||||
@@ -749,6 +915,63 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.60.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/70/03e9d89a053caff6ae46053890eba8e4a5665a7c5638279ed4492e6d4b8b/fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28", size = 2810747, upload-time = "2025-09-29T21:10:59.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/41/449ad5aff9670ab0df0f61ee593906b67a36d7e0b4d0cd7fa41ac0325bf5/fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15", size = 2346909, upload-time = "2025-09-29T21:11:02.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/18/e5970aa96c8fad1cb19a9479cc3b7602c0c98d250fcdc06a5da994309c50/fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c", size = 4864572, upload-time = "2025-09-29T21:11:05.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/20/9b2b4051b6ec6689480787d506b5003f72648f50972a92d04527a456192c/fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", size = 4794635, upload-time = "2025-09-29T21:11:08.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/52/c791f57347c1be98f8345e3dca4ac483eb97666dd7c47f3059aeffab8b59/fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", size = 4843878, upload-time = "2025-09-29T21:11:10.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/e9/35c24a8d01644cee8c090a22fad34d5b61d1e0a8ecbc9945ad785ebf2e9e/fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", size = 4954555, upload-time = "2025-09-29T21:11:13.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/86/fb1e994971be4bdfe3a307de6373ef69a9df83fb66e3faa9c8114893d4cc/fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", size = 2232019, upload-time = "2025-09-29T21:11:15.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/84/62a19e2bd56f0e9fb347486a5b26376bade4bf6bbba64dda2c103bd08c94/fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", size = 2276803, upload-time = "2025-09-29T21:11:18.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fqdn"
|
||||
version = "1.5.1"
|
||||
@@ -1374,6 +1597,114 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/6a/ca128561b22b60bd5a0c4ea26649e68c8556b82bc70a0c396eebc977fe86/jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c", size = 216571, upload-time = "2025-05-05T12:32:29.534Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lark"
|
||||
version = "1.3.0"
|
||||
@@ -1419,6 +1750,8 @@ dependencies = [
|
||||
{ name = "hf-proxy" },
|
||||
{ name = "ipykernel" },
|
||||
{ name = "llm" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "torch" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
|
||||
@@ -1443,11 +1776,13 @@ requires-dist = [
|
||||
{ name = "ipykernel" },
|
||||
{ name = "jupyter", marker = "extra == 'dev'", specifier = ">=1.0.0" },
|
||||
{ name = "llm", editable = "llm" },
|
||||
{ name = "matplotlib", specifier = "==3.10.6" },
|
||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" },
|
||||
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" },
|
||||
{ name = "torch", specifier = "==2.8.0" },
|
||||
{ name = "tqdm", specifier = ">=4,<5" },
|
||||
]
|
||||
provides-extras = ["dev", "test"]
|
||||
@@ -1537,6 +1872,81 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "cycler" },
|
||||
{ name = "fonttools" },
|
||||
{ name = "kiwisolver" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pyparsing" },
|
||||
{ name = "python-dateutil" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a0/59/c3e6453a9676ffba145309a73c462bb407f4400de7de3f2b41af70720a3c/matplotlib-3.10.6.tar.gz", hash = "sha256:ec01b645840dd1996df21ee37f208cd8ba57644779fa20464010638013d3203c", size = 34804264, upload-time = "2025-08-30T00:14:25.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/dc/ab89f7a5efd0cbaaebf2c3cf1881f4cba20c8925bb43f64511059df76895/matplotlib-3.10.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bc7316c306d97463a9866b89d5cc217824e799fa0de346c8f68f4f3d27c8693d", size = 8247159, upload-time = "2025-08-30T00:12:30.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a5/ddaee1a383ab28174093644fff7438eddb87bf8dbd58f7b85f5cdd6b2485/matplotlib-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d00932b0d160ef03f59f9c0e16d1e3ac89646f7785165ce6ad40c842db16cc2e", size = 8108011, upload-time = "2025-08-30T00:12:32.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/5b/a53f69bb0522db352b1135bb57cd9fe00fd7252072409392d991d3a755d0/matplotlib-3.10.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fa4c43d6bfdbfec09c733bca8667de11bfa4970e8324c471f3a3632a0301c15", size = 8680518, upload-time = "2025-08-30T00:12:34.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/31/e059ddce95f68819b005a2d6820b2d6ed0307827a04598891f00649bed2d/matplotlib-3.10.6-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea117a9c1627acaa04dbf36265691921b999cbf515a015298e54e1a12c3af837", size = 9514997, upload-time = "2025-08-30T00:12:36.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/d5/28b408a7c0f07b41577ee27e4454fe329e78ca21fe46ae7a27d279165fb5/matplotlib-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08fc803293b4e1694ee325896030de97f74c141ccff0be886bb5915269247676", size = 9566440, upload-time = "2025-08-30T00:12:41.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/99/8325b3386b479b1d182ab1a7fd588fd393ff00a99dc04b7cf7d06668cf0f/matplotlib-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:2adf92d9b7527fbfb8818e050260f0ebaa460f79d61546374ce73506c9421d09", size = 8108186, upload-time = "2025-08-30T00:12:43.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/5d3665aa44c49005aaacaa68ddea6fcb27345961cd538a98bb0177934ede/matplotlib-3.10.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:905b60d1cb0ee604ce65b297b61cf8be9f4e6cfecf95a3fe1c388b5266bc8f4f", size = 8257527, upload-time = "2025-08-30T00:12:45.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/af/30ddefe19ca67eebd70047dabf50f899eaff6f3c5e6a1a7edaecaf63f794/matplotlib-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bac38d816637343e53d7185d0c66677ff30ffb131044a81898b5792c956ba76", size = 8119583, upload-time = "2025-08-30T00:12:47.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/29/4a8650a3dcae97fa4f375d46efcb25920d67b512186f8a6788b896062a81/matplotlib-3.10.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:942a8de2b5bfff1de31d95722f702e2966b8a7e31f4e68f7cd963c7cd8861cf6", size = 8692682, upload-time = "2025-08-30T00:12:48.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/d3/b793b9cb061cfd5d42ff0f69d1822f8d5dbc94e004618e48a97a8373179a/matplotlib-3.10.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3276c85370bc0dfca051ec65c5817d1e0f8f5ce1b7787528ec8ed2d524bbc2f", size = 9521065, upload-time = "2025-08-30T00:12:50.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/c5/53de5629f223c1c66668d46ac2621961970d21916a4bc3862b174eb2a88f/matplotlib-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9df5851b219225731f564e4b9e7f2ac1e13c9e6481f941b5631a0f8e2d9387ce", size = 9576888, upload-time = "2025-08-30T00:12:52.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/8e/0a18d6d7d2d0a2e66585032a760d13662e5250c784d53ad50434e9560991/matplotlib-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:abb5d9478625dd9c9eb51a06d39aae71eda749ae9b3138afb23eb38824026c7e", size = 8115158, upload-time = "2025-08-30T00:12:54.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b3/1a5107bb66c261e23b9338070702597a2d374e5aa7004b7adfc754fbed02/matplotlib-3.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:886f989ccfae63659183173bb3fced7fd65e9eb793c3cc21c273add368536951", size = 7992444, upload-time = "2025-08-30T00:12:57.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/1a/7042f7430055d567cc3257ac409fcf608599ab27459457f13772c2d9778b/matplotlib-3.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ca662df6a80bd426f871105fdd69db7543e28e73a9f2afe80de7e531eb2347", size = 8272404, upload-time = "2025-08-30T00:12:59.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/5d/1d5f33f5b43f4f9e69e6a5fe1fb9090936ae7bc8e2ff6158e7a76542633b/matplotlib-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1678bb61d897bb4ac4757b5ecfb02bfb3fddf7f808000fb81e09c510712fda75", size = 8128262, upload-time = "2025-08-30T00:13:01.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/c3/135fdbbbf84e0979712df58e5e22b4f257b3f5e52a3c4aacf1b8abec0d09/matplotlib-3.10.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:56cd2d20842f58c03d2d6e6c1f1cf5548ad6f66b91e1e48f814e4fb5abd1cb95", size = 8697008, upload-time = "2025-08-30T00:13:03.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/be/c443ea428fb2488a3ea7608714b1bd85a82738c45da21b447dc49e2f8e5d/matplotlib-3.10.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:662df55604a2f9a45435566d6e2660e41efe83cd94f4288dfbf1e6d1eae4b0bb", size = 9530166, upload-time = "2025-08-30T00:13:05.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/35/48441422b044d74034aea2a3e0d1a49023f12150ebc58f16600132b9bbaf/matplotlib-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08f141d55148cd1fc870c3387d70ca4df16dee10e909b3b038782bd4bda6ea07", size = 9593105, upload-time = "2025-08-30T00:13:08.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/c3/994ef20eb4154ab84cc08d033834555319e4af970165e6c8894050af0b3c/matplotlib-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:590f5925c2d650b5c9d813c5b3b5fc53f2929c3f8ef463e4ecfa7e052044fb2b", size = 8122784, upload-time = "2025-08-30T00:13:10.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b8/5c85d9ae0e40f04e71bedb053aada5d6bab1f9b5399a0937afb5d6b02d98/matplotlib-3.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:f44c8d264a71609c79a78d50349e724f5d5fc3684ead7c2a473665ee63d868aa", size = 7992823, upload-time = "2025-08-30T00:13:12.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/db/18380e788bb837e724358287b08e223b32bc8dccb3b0c12fa8ca20bc7f3b/matplotlib-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:819e409653c1106c8deaf62e6de6b8611449c2cd9939acb0d7d4e57a3d95cc7a", size = 8273231, upload-time = "2025-08-30T00:13:13.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/0f/38dd49445b297e0d4f12a322c30779df0d43cb5873c7847df8a82e82ec67/matplotlib-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59c8ac8382fefb9cb71308dde16a7c487432f5255d8f1fd32473523abecfecdf", size = 8128730, upload-time = "2025-08-30T00:13:15.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84e82d9e0fd70c70bc55739defbd8055c54300750cbacf4740c9673a24d6933a", size = 8698539, upload-time = "2025-08-30T00:13:17.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/34/44c7b1f075e1ea398f88aeabcc2907c01b9cc99e2afd560c1d49845a1227/matplotlib-3.10.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25f7a3eb42d6c1c56e89eacd495661fc815ffc08d9da750bca766771c0fd9110", size = 9529702, upload-time = "2025-08-30T00:13:19.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/7f/e5c2dc9950c7facaf8b461858d1b92c09dd0cf174fe14e21953b3dda06f7/matplotlib-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9c862d91ec0b7842920a4cfdaaec29662195301914ea54c33e01f1a28d014b2", size = 9593742, upload-time = "2025-08-30T00:13:21.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/1d/70c28528794f6410ee2856cd729fa1f1756498b8d3126443b0a94e1a8695/matplotlib-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:1b53bd6337eba483e2e7d29c5ab10eee644bc3a2491ec67cc55f7b44583ffb18", size = 8122753, upload-time = "2025-08-30T00:13:23.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/74/0e1670501fc7d02d981564caf7c4df42974464625935424ca9654040077c/matplotlib-3.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:cbd5eb50b7058b2892ce45c2f4e92557f395c9991f5c886d1bb74a1582e70fd6", size = 7992973, upload-time = "2025-08-30T00:13:26.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/4e/60780e631d73b6b02bd7239f89c451a72970e5e7ec34f621eda55cd9a445/matplotlib-3.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:acc86dd6e0e695c095001a7fccff158c49e45e0758fdf5dcdbb0103318b59c9f", size = 8316869, upload-time = "2025-08-30T00:13:28.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/15/baa662374a579413210fc2115d40c503b7360a08e9cc254aa0d97d34b0c1/matplotlib-3.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e228cd2ffb8f88b7d0b29e37f68ca9aaf83e33821f24a5ccc4f082dd8396bc27", size = 8178240, upload-time = "2025-08-30T00:13:30.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/3f/3c38e78d2aafdb8829fcd0857d25aaf9e7dd2dfcf7ec742765b585774931/matplotlib-3.10.6-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:658bc91894adeab669cf4bb4a186d049948262987e80f0857216387d7435d833", size = 8711719, upload-time = "2025-08-30T00:13:31.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/4b/2ec2bbf8cefaa53207cc56118d1fa8a0f9b80642713ea9390235d331ede4/matplotlib-3.10.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8913b7474f6dd83ac444c9459c91f7f0f2859e839f41d642691b104e0af056aa", size = 9541422, upload-time = "2025-08-30T00:13:33.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/7d/40255e89b3ef11c7871020563b2dd85f6cb1b4eff17c0f62b6eb14c8fa80/matplotlib-3.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:091cea22e059b89f6d7d1a18e2c33a7376c26eee60e401d92a4d6726c4e12706", size = 9594068, upload-time = "2025-08-30T00:13:35.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/a9/0213748d69dc842537a113493e1c27daf9f96bd7cc316f933dc8ec4de985/matplotlib-3.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:491e25e02a23d7207629d942c666924a6b61e007a48177fdd231a0097b7f507e", size = 8200100, upload-time = "2025-08-30T00:13:37.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/15/79f9988066ce40b8a6f1759a934ea0cde8dc4adc2262255ee1bc98de6ad0/matplotlib-3.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3d80d60d4e54cda462e2cd9a086d85cd9f20943ead92f575ce86885a43a565d5", size = 8042142, upload-time = "2025-08-30T00:13:39.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/58/e7b6d292beae6fb4283ca6fb7fa47d7c944a68062d6238c07b497dd35493/matplotlib-3.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:70aaf890ce1d0efd482df969b28a5b30ea0b891224bb315810a3940f67182899", size = 8273802, upload-time = "2025-08-30T00:13:41.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/f6/7882d05aba16a8cdd594fb9a03a9d3cca751dbb6816adf7b102945522ee9/matplotlib-3.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1565aae810ab79cb72e402b22facfa6501365e73ebab70a0fdfb98488d2c3c0c", size = 8131365, upload-time = "2025-08-30T00:13:42.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/bf/ff32f6ed76e78514e98775a53715eca4804b12bdcf35902cdd1cf759d324/matplotlib-3.10.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3b23315a01981689aa4e1a179dbf6ef9fbd17143c3eea77548c2ecfb0499438", size = 9533961, upload-time = "2025-08-30T00:13:44.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/c3/6bf88c2fc2da7708a2ff8d2eeb5d68943130f50e636d5d3dcf9d4252e971/matplotlib-3.10.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30fdd37edf41a4e6785f9b37969de57aea770696cb637d9946eb37470c94a453", size = 9804262, upload-time = "2025-08-30T00:13:46.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/7a/e05e6d9446d2d577b459427ad060cd2de5742d0e435db3191fea4fcc7e8b/matplotlib-3.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc31e693da1c08012c764b053e702c1855378e04102238e6a5ee6a7117c53a47", size = 9595508, upload-time = "2025-08-30T00:13:48.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/fb/af09c463ced80b801629fd73b96f726c9f6124c3603aa2e480a061d6705b/matplotlib-3.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:05be9bdaa8b242bc6ff96330d18c52f1fc59c6fb3a4dd411d953d67e7e1baf98", size = 8252742, upload-time = "2025-08-30T00:13:50.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f9/b682f6db9396d9ab8f050c0a3bfbb5f14fb0f6518f08507c04cc02f8f229/matplotlib-3.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:f56a0d1ab05d34c628592435781d185cd99630bdfd76822cd686fb5a0aecd43a", size = 8124237, upload-time = "2025-08-30T00:13:54.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/d2/b69b4a0923a3c05ab90527c60fdec899ee21ca23ede7f0fb818e6620d6f2/matplotlib-3.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:94f0b4cacb23763b64b5dace50d5b7bfe98710fed5f0cef5c08135a03399d98b", size = 8316956, upload-time = "2025-08-30T00:13:55.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/e9/dc427b6f16457ffaeecb2fc4abf91e5adb8827861b869c7a7a6d1836fa73/matplotlib-3.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cc332891306b9fb39462673d8225d1b824c89783fee82840a709f96714f17a5c", size = 8178260, upload-time = "2025-08-30T00:14:00.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/89/1fbd5ad611802c34d1c7ad04607e64a1350b7fb9c567c4ec2c19e066ed35/matplotlib-3.10.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee1d607b3fb1590deb04b69f02ea1d53ed0b0bf75b2b1a5745f269afcbd3cdd3", size = 9541422, upload-time = "2025-08-30T00:14:02.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3b/65fec8716025b22c1d72d5a82ea079934c76a547696eaa55be6866bc89b1/matplotlib-3.10.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:376a624a218116461696b27b2bbf7a8945053e6d799f6502fc03226d077807bf", size = 9803678, upload-time = "2025-08-30T00:14:04.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b0/40fb2b3a1ab9381bb39a952e8390357c8be3bdadcf6d5055d9c31e1b35ae/matplotlib-3.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:83847b47f6524c34b4f2d3ce726bb0541c48c8e7692729865c3df75bfa0f495a", size = 9594077, upload-time = "2025-08-30T00:14:07.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/34/c4b71b69edf5b06e635eee1ed10bfc73cf8df058b66e63e30e6a55e231d5/matplotlib-3.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c7e0518e0d223683532a07f4b512e2e0729b62674f1b3a1a69869f98e6b1c7e3", size = 8342822, upload-time = "2025-08-30T00:14:09.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/62/aeabeef1a842b6226a30d49dd13e8a7a1e81e9ec98212c0b5169f0a12d83/matplotlib-3.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:4dd83e029f5b4801eeb87c64efd80e732452781c16a9cf7415b7b63ec8f374d7", size = 8172588, upload-time = "2025-08-30T00:14:11.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/6f/2551e45bea2938e0363ccdd54fa08dae7605ce782d4332497d31a7b97672/matplotlib-3.10.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:13fcd07ccf17e354398358e0307a1f53f5325dca22982556ddb9c52837b5af41", size = 8241220, upload-time = "2025-08-30T00:14:12.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7e/0f4c6e8b98105fdb162a4efde011af204ca47d7c05d735aff480ebfead1b/matplotlib-3.10.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:470fc846d59d1406e34fa4c32ba371039cd12c2fe86801159a965956f2575bd1", size = 8104624, upload-time = "2025-08-30T00:14:14.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/27/c29696702b9317a6ade1ba6f8861e02d7423f18501729203d7a80b686f23/matplotlib-3.10.6-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7173f8551b88f4ef810a94adae3128c2530e0d07529f7141be7f8d8c365f051", size = 8682271, upload-time = "2025-08-30T00:14:17.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/bb/02c35a51484aae5f49bd29f091286e7af5f3f677a9736c58a92b3c78baeb/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f2d684c3204fa62421bbf770ddfebc6b50130f9cad65531eeba19236d73bb488", size = 8252296, upload-time = "2025-08-30T00:14:19.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/85/41701e3092005aee9a2445f5ee3904d9dbd4a7df7a45905ffef29b7ef098/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6f4a69196e663a41d12a728fab8751177215357906436804217d6d9cf0d4d6cf", size = 8116749, upload-time = "2025-08-30T00:14:21.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/53/8d8fa0ea32a8c8239e04d022f6c059ee5e1b77517769feccd50f1df43d6d/matplotlib-3.10.6-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d6ca6ef03dfd269f4ead566ec6f3fb9becf8dab146fb999022ed85ee9f6b3eb", size = 8693933, upload-time = "2025-08-30T00:14:22.942Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib-inline"
|
||||
version = "0.1.7"
|
||||
@@ -2258,6 +2668,108 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.4.0"
|
||||
@@ -2481,6 +2993,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
|
||||
Reference in New Issue
Block a user