Upload folder using huggingface_hub
Browse files- .gitattributes +2 -34
- LICENSE +21 -0
- MODEL_CARD.md +58 -0
- README.md +80 -3
- config.json +14 -0
- configs/ultra_mini.json +14 -0
- data/README.md +31 -0
- data/corpus.txt +0 -0
- data/delta_seed.jsonl +0 -0
- data/tokenizer_corpus.txt +0 -0
- delta/__init__.py +6 -0
- delta/dataset.py +101 -0
- delta/generator.py +197 -0
- delta/identity.py +93 -0
- delta/model.py +331 -0
- delta/tokenizer.py +137 -0
- delta/trainer.py +106 -0
- delta_checkpoint.pt +3 -0
- model.safetensors +3 -0
- requirements.txt +4 -0
- scripts/generate_delta.py +58 -0
- scripts/train_delta.py +58 -0
- scripts/train_tokenizer.py +39 -0
- tokenizer.json +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,3 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
|
| 27 |
-
*.
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Flame Corporation
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
MODEL_CARD.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- pt
|
| 5 |
+
- en
|
| 6 |
+
pipeline_tag: text-generation
|
| 7 |
+
tags:
|
| 8 |
+
- pytorch
|
| 9 |
+
- causal-lm
|
| 10 |
+
- decoder-only
|
| 11 |
+
- educational
|
| 12 |
+
- small-language-model
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# Delta Ultra Mini
|
| 16 |
+
|
| 17 |
+
Delta Ultra Mini is a compact decoder-only language model created by Flame Corporation. It is intended as an educational small LLM release for studying tokenizer training, causal language modeling, checkpoints, and local generation.
|
| 18 |
+
|
| 19 |
+
## Model Details
|
| 20 |
+
|
| 21 |
+
- Architecture: decoder-only causal Transformer
|
| 22 |
+
- Parameters: about 50M
|
| 23 |
+
- Context length: 512 tokens
|
| 24 |
+
- Tokenizer: BPE
|
| 25 |
+
- License: MIT
|
| 26 |
+
|
| 27 |
+
## Intended Use
|
| 28 |
+
|
| 29 |
+
- Learning how small language models are structured and trained
|
| 30 |
+
- Running local inference experiments
|
| 31 |
+
- Building and testing custom small datasets
|
| 32 |
+
- Fine-tuning from a compact baseline
|
| 33 |
+
|
| 34 |
+
## Not Intended For
|
| 35 |
+
|
| 36 |
+
- Production assistant use without evaluation
|
| 37 |
+
- High-stakes decision making
|
| 38 |
+
- Security, medical, legal, or financial advice
|
| 39 |
+
- Claims of strong general reasoning
|
| 40 |
+
|
| 41 |
+
## Training Data
|
| 42 |
+
|
| 43 |
+
The seed dataset is small and conversational. It is designed to validate the training pipeline, not to produce a high-quality assistant on its own. Users are encouraged to create larger, cleaner datasets and train new checkpoints.
|
| 44 |
+
|
| 45 |
+
## Limitations
|
| 46 |
+
|
| 47 |
+
Delta Ultra Mini can hallucinate, mix examples from the seed dataset, and answer incorrectly. The included checkpoint should be treated as an experimental baseline.
|
| 48 |
+
|
| 49 |
+
## Quick Start
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
pip install -r requirements.txt
|
| 53 |
+
python scripts/generate_delta.py --prompt "Quem e voce?" --checkpoint_path delta_checkpoint.pt --tokenizer_path tokenizer.json
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Citation
|
| 57 |
+
|
| 58 |
+
If you publish work using this model, cite the repository or model page where you downloaded it.
|
README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Delta Ultra Mini
|
| 2 |
+
|
| 3 |
+
Delta Ultra Mini is a compact decoder-only language model created by Flame Corporation. This model-only release contains the neural model code, tokenizer wrapper, training utilities, seed dataset, and simple local inference tools.
|
| 4 |
+
|
| 5 |
+
This release intentionally does not include the REST API, API key server, browser SDK, or Python HTTP SDK.
|
| 6 |
+
|
| 7 |
+
## Model
|
| 8 |
+
|
| 9 |
+
- Architecture: decoder-only causal Transformer
|
| 10 |
+
- Parameters: about 50M
|
| 11 |
+
- Context length: 512 tokens
|
| 12 |
+
- Tokenizer: BPE with chat tokens
|
| 13 |
+
- License: MIT
|
| 14 |
+
|
| 15 |
+
Delta Ultra Mini is an educational and experimental small LLM. It is useful for learning how a compact language model is structured, trained, checkpointed, and sampled. It is not a strong general assistant yet.
|
| 16 |
+
|
| 17 |
+
## Install
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install -r requirements.txt
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Files
|
| 24 |
+
|
| 25 |
+
- `delta/model.py`: Transformer model
|
| 26 |
+
- `delta/tokenizer.py`: tokenizer training/loading and chat formatting
|
| 27 |
+
- `delta/generator.py`: local autoregressive generation
|
| 28 |
+
- `delta/dataset.py`: text/jsonl dataset loader
|
| 29 |
+
- `delta/trainer.py`: HuggingFace Trainer integration
|
| 30 |
+
- `configs/ultra_mini.json`: model configuration
|
| 31 |
+
- `tokenizer.json`: trained tokenizer
|
| 32 |
+
- `data/`: small MIT-licensed seed dataset
|
| 33 |
+
- `scripts/train_tokenizer.py`: tokenizer training entrypoint
|
| 34 |
+
- `scripts/train_delta.py`: model training entrypoint
|
| 35 |
+
- `scripts/generate_delta.py`: local inference entrypoint
|
| 36 |
+
|
| 37 |
+
## Local Inference
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
python scripts/generate_delta.py --prompt "O que e PyTorch?" --checkpoint_path runs/delta-ultra-mini/delta_checkpoint.pt --tokenizer_path tokenizer.json
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
If your checkpoint is at the release root, use:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
python scripts/generate_delta.py --prompt "Quem e voce?" --checkpoint_path delta_checkpoint.pt --tokenizer_path tokenizer.json
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Train Tokenizer
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
python scripts/train_tokenizer.py --corpus_files data/tokenizer_corpus.txt --output_path tokenizer.json
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## Train Model
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
python scripts/train_delta.py --data_path data --output_dir runs/delta-ultra-mini --epochs 1 --batch_size 2 --tokenizer_path tokenizer.json
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## Dataset
|
| 62 |
+
|
| 63 |
+
The included dataset is a small seed dataset. It is meant to bootstrap experimentation and verify the pipeline. For better quality, create a larger dataset with varied examples, clean answers, validation splits, and careful review.
|
| 64 |
+
|
| 65 |
+
Recommended format:
|
| 66 |
+
|
| 67 |
+
```jsonl
|
| 68 |
+
{"text":"[SYS] You are Delta. [SEP]\n[USR] Question [SEP]\n[ASS] Answer [SEP]"}
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Limitations
|
| 72 |
+
|
| 73 |
+
- The seed checkpoint may memorize examples and generalize poorly.
|
| 74 |
+
- The model is not safety-aligned like large production assistants.
|
| 75 |
+
- It can produce incorrect or mixed answers.
|
| 76 |
+
- It should be evaluated before any real use.
|
| 77 |
+
|
| 78 |
+
## License
|
| 79 |
+
|
| 80 |
+
MIT.
|
config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"vocab_size": 32000,
|
| 3 |
+
"d_model": 512,
|
| 4 |
+
"n_heads": 8,
|
| 5 |
+
"n_layers": 8,
|
| 6 |
+
"d_ff": 2048,
|
| 7 |
+
"max_seq_len": 512,
|
| 8 |
+
"dropout": 0.1,
|
| 9 |
+
"tie_embeddings": true,
|
| 10 |
+
"pad_token_id": 0,
|
| 11 |
+
"bos_token_id": 2,
|
| 12 |
+
"eos_token_id": 3,
|
| 13 |
+
"use_cache": false
|
| 14 |
+
}
|
configs/ultra_mini.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"vocab_size": 32000,
|
| 3 |
+
"d_model": 512,
|
| 4 |
+
"n_heads": 8,
|
| 5 |
+
"n_layers": 8,
|
| 6 |
+
"d_ff": 2048,
|
| 7 |
+
"max_seq_len": 512,
|
| 8 |
+
"dropout": 0.1,
|
| 9 |
+
"tie_embeddings": true,
|
| 10 |
+
"pad_token_id": 0,
|
| 11 |
+
"bos_token_id": 2,
|
| 12 |
+
"eos_token_id": 3,
|
| 13 |
+
"use_cache": true
|
| 14 |
+
}
|
data/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Delta Seed Dataset
|
| 2 |
+
|
| 3 |
+
This directory contains the small seed dataset used to test and bootstrap Delta Ultra Mini.
|
| 4 |
+
|
| 5 |
+
- `delta_seed.jsonl`: conversational JSONL records with a `text` field.
|
| 6 |
+
- `tokenizer_corpus.txt`: plain text corpus for BPE tokenizer training.
|
| 7 |
+
- `corpus.txt`: training text in the same chat-token format.
|
| 8 |
+
|
| 9 |
+
This dataset is intentionally small. It is useful for validating the tokenizer, dataset loader, training loop, checkpoint saving, and local inference. It is not enough to create a strong general assistant.
|
| 10 |
+
|
| 11 |
+
## License
|
| 12 |
+
|
| 13 |
+
MIT, same as the project.
|
| 14 |
+
|
| 15 |
+
## Format
|
| 16 |
+
|
| 17 |
+
```jsonl
|
| 18 |
+
{"text":"[SYS] You are Delta. [SEP]\n[USR] Question [SEP]\n[ASS] Answer [SEP]"}
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
## Train Tokenizer
|
| 22 |
+
|
| 23 |
+
```powershell
|
| 24 |
+
python scripts/train_tokenizer.py --corpus_files data/tokenizer_corpus.txt --output_path tokenizer.json
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Train Model
|
| 28 |
+
|
| 29 |
+
```powershell
|
| 30 |
+
python scripts/train_delta.py --data_path data --output_dir runs/delta-ultra-mini --epochs 1 --batch_size 2 --tokenizer_path tokenizer.json
|
| 31 |
+
```
|
data/corpus.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/delta_seed.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/tokenizer_corpus.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
delta/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Delta Ultra Mini package."""
|
| 2 |
+
|
| 3 |
+
from delta.identity import get_identity
|
| 4 |
+
from delta.model import DeltaConfig, DeltaModel
|
| 5 |
+
|
| 6 |
+
__all__ = ["DeltaConfig", "DeltaModel", "get_identity"]
|
delta/dataset.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dataset and collator for Delta causal language modeling."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from torch.utils.data import Dataset
|
| 13 |
+
|
| 14 |
+
from delta.tokenizer import DeltaTokenizer
|
| 15 |
+
|
| 16 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _read_texts(path: Path) -> list[str]:
|
| 21 |
+
"""Read .txt files or .jsonl files containing a text field."""
|
| 22 |
+
|
| 23 |
+
files = [path] if path.is_file() else sorted(path.rglob("*"))
|
| 24 |
+
texts: list[str] = []
|
| 25 |
+
for file_path in files:
|
| 26 |
+
if file_path.suffix.lower() == ".txt":
|
| 27 |
+
texts.append(file_path.read_text(encoding="utf-8"))
|
| 28 |
+
elif file_path.suffix.lower() == ".jsonl":
|
| 29 |
+
with file_path.open("r", encoding="utf-8") as handle:
|
| 30 |
+
for line in handle:
|
| 31 |
+
line = line.strip()
|
| 32 |
+
if not line:
|
| 33 |
+
continue
|
| 34 |
+
record = json.loads(line)
|
| 35 |
+
if "text" in record:
|
| 36 |
+
texts.append(str(record["text"]))
|
| 37 |
+
return texts
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class DeltaDataset(Dataset[dict[str, torch.Tensor]]):
|
| 41 |
+
"""Sliding-window token dataset for language modeling."""
|
| 42 |
+
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
data_path: str | Path,
|
| 46 |
+
tokenizer: DeltaTokenizer,
|
| 47 |
+
max_seq_len: int = 512,
|
| 48 |
+
stride: int = 256,
|
| 49 |
+
) -> None:
|
| 50 |
+
self.data_path = Path(data_path)
|
| 51 |
+
self.tokenizer = tokenizer
|
| 52 |
+
self.max_seq_len = max_seq_len
|
| 53 |
+
self.stride = stride
|
| 54 |
+
texts = _read_texts(self.data_path)
|
| 55 |
+
if not texts:
|
| 56 |
+
raise ValueError(f"No .txt or .jsonl text records found in {self.data_path}")
|
| 57 |
+
self.windows: list[list[int]] = []
|
| 58 |
+
for text in texts:
|
| 59 |
+
ids = tokenizer.encode(text, add_special_tokens=True)
|
| 60 |
+
for start in range(0, max(1, len(ids) - 1), stride):
|
| 61 |
+
window = ids[start : start + max_seq_len]
|
| 62 |
+
if len(window) >= 2:
|
| 63 |
+
self.windows.append(window)
|
| 64 |
+
if start + max_seq_len >= len(ids):
|
| 65 |
+
break
|
| 66 |
+
logger.info("Loaded %s training windows from %s", len(self.windows), self.data_path)
|
| 67 |
+
|
| 68 |
+
def __len__(self) -> int:
|
| 69 |
+
"""Return the number of windows."""
|
| 70 |
+
|
| 71 |
+
return len(self.windows)
|
| 72 |
+
|
| 73 |
+
def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
|
| 74 |
+
"""Return one token window."""
|
| 75 |
+
|
| 76 |
+
ids = torch.tensor(self.windows[index], dtype=torch.long)
|
| 77 |
+
return {"input_ids": ids, "labels": ids.clone()}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class DeltaDataCollator:
|
| 81 |
+
"""Dynamic padding collator for causal language modeling."""
|
| 82 |
+
|
| 83 |
+
def __init__(self, pad_token_id: int = 0) -> None:
|
| 84 |
+
self.pad_token_id = pad_token_id
|
| 85 |
+
|
| 86 |
+
def __call__(self, features: list[dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]:
|
| 87 |
+
"""Pad input ids and labels to the longest sample in the batch."""
|
| 88 |
+
|
| 89 |
+
max_len = max(feature["input_ids"].size(0) for feature in features)
|
| 90 |
+
input_ids = torch.full((len(features), max_len), self.pad_token_id, dtype=torch.long)
|
| 91 |
+
labels = torch.full((len(features), max_len), -100, dtype=torch.long)
|
| 92 |
+
attention_mask = torch.zeros((len(features), max_len), dtype=torch.long)
|
| 93 |
+
for row, feature in enumerate(features):
|
| 94 |
+
ids = feature["input_ids"]
|
| 95 |
+
length = ids.size(0)
|
| 96 |
+
input_ids[row, :length] = ids
|
| 97 |
+
labels[row, :length] = feature["labels"]
|
| 98 |
+
pad_positions = ids == self.pad_token_id
|
| 99 |
+
labels[row, :length][pad_positions] = -100
|
| 100 |
+
attention_mask[row, :length] = 1
|
| 101 |
+
return {"input_ids": input_ids, "labels": labels, "attention_mask": attention_mask}
|
delta/generator.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Text generation and chat helpers for Delta Ultra Mini."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Generator
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch.nn import functional as F
|
| 12 |
+
|
| 13 |
+
from delta.identity import identity_response
|
| 14 |
+
from delta.model import DeltaConfig, DeltaModel
|
| 15 |
+
from delta.tokenizer import DEFAULT_SYSTEM_PROMPT, DeltaTokenizer
|
| 16 |
+
|
| 17 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class DeltaGenerator:
|
| 22 |
+
"""Autoregressive generator for Delta Ultra Mini."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, model: DeltaModel, tokenizer: DeltaTokenizer, device: str | torch.device | None = None) -> None:
|
| 25 |
+
self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
|
| 26 |
+
self.model = model.to(self.device)
|
| 27 |
+
self.model.eval()
|
| 28 |
+
self.tokenizer = tokenizer
|
| 29 |
+
|
| 30 |
+
@classmethod
|
| 31 |
+
def from_files(
|
| 32 |
+
cls,
|
| 33 |
+
checkpoint_path: str | Path,
|
| 34 |
+
tokenizer_path: str | Path,
|
| 35 |
+
config_path: str | Path | None = None,
|
| 36 |
+
device: str | torch.device | None = None,
|
| 37 |
+
) -> "DeltaGenerator":
|
| 38 |
+
"""Create a generator from checkpoint, tokenizer, and optional config."""
|
| 39 |
+
|
| 40 |
+
checkpoint = torch.load(checkpoint_path, map_location="cpu")
|
| 41 |
+
config_data = checkpoint.get("config")
|
| 42 |
+
config = DeltaConfig.from_dict(config_data) if config_data else DeltaConfig.from_json(config_path or "configs/ultra_mini.json")
|
| 43 |
+
model = DeltaModel(config)
|
| 44 |
+
state = checkpoint.get("model_state_dict", checkpoint)
|
| 45 |
+
model.load_state_dict(state)
|
| 46 |
+
return cls(model=model, tokenizer=DeltaTokenizer(tokenizer_path), device=device)
|
| 47 |
+
|
| 48 |
+
def _filter_logits(
|
| 49 |
+
self,
|
| 50 |
+
logits: torch.Tensor,
|
| 51 |
+
temperature: float,
|
| 52 |
+
top_k: int,
|
| 53 |
+
top_p: float,
|
| 54 |
+
) -> torch.Tensor:
|
| 55 |
+
"""Apply temperature, top-k, and nucleus filtering."""
|
| 56 |
+
|
| 57 |
+
if temperature <= 0:
|
| 58 |
+
return logits
|
| 59 |
+
logits = logits / temperature
|
| 60 |
+
if top_k > 0:
|
| 61 |
+
values, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
| 62 |
+
logits = logits.masked_fill(logits < values[:, [-1]], torch.finfo(logits.dtype).min)
|
| 63 |
+
if 0.0 < top_p < 1.0:
|
| 64 |
+
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
|
| 65 |
+
sorted_probs = F.softmax(sorted_logits, dim=-1)
|
| 66 |
+
cumulative = sorted_probs.cumsum(dim=-1)
|
| 67 |
+
remove = cumulative > top_p
|
| 68 |
+
remove[..., 1:] = remove[..., :-1].clone()
|
| 69 |
+
remove[..., 0] = False
|
| 70 |
+
indices_to_remove = remove.scatter(1, sorted_indices, remove)
|
| 71 |
+
logits = logits.masked_fill(indices_to_remove, torch.finfo(logits.dtype).min)
|
| 72 |
+
return logits
|
| 73 |
+
|
| 74 |
+
def _apply_repetition_penalty(
|
| 75 |
+
self,
|
| 76 |
+
logits: torch.Tensor,
|
| 77 |
+
generated: torch.Tensor,
|
| 78 |
+
repetition_penalty: float,
|
| 79 |
+
) -> torch.Tensor:
|
| 80 |
+
"""Penalize tokens that already appeared in the generated sequence."""
|
| 81 |
+
|
| 82 |
+
if repetition_penalty == 1.0:
|
| 83 |
+
return logits
|
| 84 |
+
for token_id in set(generated[0].tolist()):
|
| 85 |
+
score = logits[:, token_id]
|
| 86 |
+
logits[:, token_id] = torch.where(score < 0, score * repetition_penalty, score / repetition_penalty)
|
| 87 |
+
return logits
|
| 88 |
+
|
| 89 |
+
def _clean_completion_text(self, text: str) -> str:
|
| 90 |
+
"""Trim leaked prompt/chat markers from decoded assistant text."""
|
| 91 |
+
|
| 92 |
+
cut_markers = ("[SYS]", "[USR]", "[ASS]", "[SEP]", DEFAULT_SYSTEM_PROMPT)
|
| 93 |
+
clean = text
|
| 94 |
+
for marker in cut_markers:
|
| 95 |
+
index = clean.find(marker)
|
| 96 |
+
if index >= 0:
|
| 97 |
+
clean = clean[:index]
|
| 98 |
+
return clean.strip()
|
| 99 |
+
|
| 100 |
+
@torch.inference_mode()
|
| 101 |
+
def generate(
|
| 102 |
+
self,
|
| 103 |
+
input_ids: list[int] | torch.Tensor,
|
| 104 |
+
max_new_tokens: int = 256,
|
| 105 |
+
temperature: float = 0.2,
|
| 106 |
+
top_k: int = 20,
|
| 107 |
+
top_p: float = 0.9,
|
| 108 |
+
repetition_penalty: float = 1.08,
|
| 109 |
+
) -> list[int]:
|
| 110 |
+
"""Generate token ids using KV cache and manual sampling."""
|
| 111 |
+
|
| 112 |
+
ids = torch.tensor([input_ids], dtype=torch.long, device=self.device) if isinstance(input_ids, list) else input_ids.to(self.device)
|
| 113 |
+
if ids.dim() == 1:
|
| 114 |
+
ids = ids.unsqueeze(0)
|
| 115 |
+
ids = ids[:, -(self.model.config.max_seq_len - 1) :]
|
| 116 |
+
max_new_tokens = min(max_new_tokens, self.model.config.max_seq_len - ids.size(1))
|
| 117 |
+
generated = ids.clone()
|
| 118 |
+
past_key_values = None
|
| 119 |
+
next_input = ids
|
| 120 |
+
stop_token_ids = self.tokenizer.chat_stop_token_ids
|
| 121 |
+
for _ in range(max_new_tokens):
|
| 122 |
+
outputs = self.model(next_input, past_key_values=past_key_values, use_cache=True)
|
| 123 |
+
logits = outputs["logits"][:, -1, :]
|
| 124 |
+
past_key_values = outputs["past_key_values"]
|
| 125 |
+
logits = self._apply_repetition_penalty(logits, generated, repetition_penalty)
|
| 126 |
+
if temperature <= 0:
|
| 127 |
+
next_token = torch.argmax(logits, dim=-1, keepdim=True)
|
| 128 |
+
else:
|
| 129 |
+
filtered = self._filter_logits(logits, temperature=temperature, top_k=top_k, top_p=top_p)
|
| 130 |
+
next_token = torch.multinomial(F.softmax(filtered, dim=-1), num_samples=1)
|
| 131 |
+
if int(next_token.item()) in stop_token_ids:
|
| 132 |
+
break
|
| 133 |
+
generated = torch.cat((generated, next_token), dim=1)
|
| 134 |
+
next_input = next_token
|
| 135 |
+
return generated[0].tolist()
|
| 136 |
+
|
| 137 |
+
def chat(self, messages: list[dict[str, Any]], persona: str | None = None, **gen_kwargs: Any) -> str:
|
| 138 |
+
"""Generate an assistant response for chat messages."""
|
| 139 |
+
|
| 140 |
+
latest_user = next((str(m.get("content", "")) for m in reversed(messages) if m.get("role") == "user"), "")
|
| 141 |
+
intercepted = identity_response(latest_user)
|
| 142 |
+
if intercepted is not None:
|
| 143 |
+
return intercepted
|
| 144 |
+
prompt = self.tokenizer.format_chat(messages, persona=persona)
|
| 145 |
+
input_ids = self.tokenizer.encode(prompt, add_special_tokens=False)
|
| 146 |
+
input_ids = input_ids[-(self.model.config.max_seq_len - 1) :]
|
| 147 |
+
output_ids = self.generate(input_ids, **gen_kwargs)
|
| 148 |
+
new_ids = output_ids[len(input_ids) :]
|
| 149 |
+
text = self.tokenizer.decode(new_ids, skip_special_tokens=True)
|
| 150 |
+
return self._clean_completion_text(text)
|
| 151 |
+
|
| 152 |
+
def stream_chat(
|
| 153 |
+
self,
|
| 154 |
+
messages: list[dict[str, Any]],
|
| 155 |
+
persona: str | None = None,
|
| 156 |
+
**gen_kwargs: Any,
|
| 157 |
+
) -> Generator[str, None, None]:
|
| 158 |
+
"""Yield generated text token by token."""
|
| 159 |
+
|
| 160 |
+
latest_user = next((str(m.get("content", "")) for m in reversed(messages) if m.get("role") == "user"), "")
|
| 161 |
+
intercepted = identity_response(latest_user)
|
| 162 |
+
if intercepted is not None:
|
| 163 |
+
yield intercepted
|
| 164 |
+
return
|
| 165 |
+
|
| 166 |
+
prompt = self.tokenizer.format_chat(messages, persona=persona)
|
| 167 |
+
input_ids = self.tokenizer.encode(prompt, add_special_tokens=False)
|
| 168 |
+
input_ids = input_ids[-(self.model.config.max_seq_len - 1) :]
|
| 169 |
+
ids = torch.tensor([input_ids], dtype=torch.long, device=self.device)
|
| 170 |
+
generated = ids.clone()
|
| 171 |
+
past_key_values = None
|
| 172 |
+
next_input = ids
|
| 173 |
+
max_new_tokens = int(gen_kwargs.get("max_new_tokens", 256))
|
| 174 |
+
max_new_tokens = min(max_new_tokens, self.model.config.max_seq_len - ids.size(1))
|
| 175 |
+
temperature = float(gen_kwargs.get("temperature", 0.2))
|
| 176 |
+
top_k = int(gen_kwargs.get("top_k", 20))
|
| 177 |
+
top_p = float(gen_kwargs.get("top_p", 0.9))
|
| 178 |
+
repetition_penalty = float(gen_kwargs.get("repetition_penalty", 1.08))
|
| 179 |
+
stop_token_ids = self.tokenizer.chat_stop_token_ids
|
| 180 |
+
with torch.inference_mode():
|
| 181 |
+
for _ in range(max_new_tokens):
|
| 182 |
+
outputs = self.model(next_input, past_key_values=past_key_values, use_cache=True)
|
| 183 |
+
logits = outputs["logits"][:, -1, :]
|
| 184 |
+
past_key_values = outputs["past_key_values"]
|
| 185 |
+
logits = self._apply_repetition_penalty(logits, generated, repetition_penalty)
|
| 186 |
+
if temperature <= 0:
|
| 187 |
+
next_token = torch.argmax(logits, dim=-1, keepdim=True)
|
| 188 |
+
else:
|
| 189 |
+
filtered = self._filter_logits(logits, temperature=temperature, top_k=top_k, top_p=top_p)
|
| 190 |
+
next_token = torch.multinomial(F.softmax(filtered, dim=-1), num_samples=1)
|
| 191 |
+
if int(next_token.item()) in stop_token_ids:
|
| 192 |
+
return
|
| 193 |
+
generated = torch.cat((generated, next_token), dim=1)
|
| 194 |
+
next_input = next_token
|
| 195 |
+
text = self.tokenizer.decode([int(next_token.item())], skip_special_tokens=True)
|
| 196 |
+
if text:
|
| 197 |
+
yield text
|
delta/identity.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hardcoded identity helpers for Delta Ultra Mini.
|
| 2 |
+
|
| 3 |
+
This module keeps the immutable public identity of the model in one place and
|
| 4 |
+
provides a small interceptor for identity questions. The interceptor is used
|
| 5 |
+
before neural generation so the model never has to infer facts about itself.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import logging
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
_IDENTITY: dict[str, str] = {
|
| 19 |
+
"name": "Delta",
|
| 20 |
+
"version": "Ultra Mini",
|
| 21 |
+
"created_by": "Flame Corporation",
|
| 22 |
+
"parameters": "~50 milhões",
|
| 23 |
+
"purpose": "Assistente conversacional inteligente via API",
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
_IDENTITY_PATTERNS: tuple[re.Pattern[str], ...] = tuple(
|
| 27 |
+
re.compile(pattern, flags=re.IGNORECASE)
|
| 28 |
+
for pattern in (
|
| 29 |
+
r"qual.*seu nome",
|
| 30 |
+
r"who are you",
|
| 31 |
+
r"quem.*voc[eê]",
|
| 32 |
+
r"sua vers[aã]o",
|
| 33 |
+
r"your version",
|
| 34 |
+
r"quem.*criou",
|
| 35 |
+
r"who made you",
|
| 36 |
+
r"quantos par[aâ]metros",
|
| 37 |
+
r"how many parameters",
|
| 38 |
+
)
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_identity() -> dict[str, str]:
|
| 43 |
+
"""Return Delta's immutable identity.
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
A copy of the identity dictionary.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
return dict(_IDENTITY)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def is_identity_question(text: str) -> bool:
|
| 53 |
+
"""Check whether a user message asks about Delta's identity.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
text: User-provided text.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
True when the text matches one of the hardcoded identity regexes.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
return any(pattern.search(text) for pattern in _IDENTITY_PATTERNS)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def identity_response(text: str) -> str | None:
|
| 66 |
+
"""Return a deterministic identity answer when the input asks for it.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
text: User-provided text.
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
A precise identity answer, or None when the input should go to the
|
| 73 |
+
neural model.
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
if not is_identity_question(text):
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
identity: dict[str, Any] = get_identity()
|
| 80 |
+
lowered = text.lower()
|
| 81 |
+
if re.search(r"quantos par[aâ]metros|how many parameters", lowered):
|
| 82 |
+
return f"Eu sou {identity['name']} {identity['version']} e tenho {identity['parameters']} de parâmetros."
|
| 83 |
+
if re.search(r"sua vers[aã]o|your version", lowered):
|
| 84 |
+
return f"Minha versão é {identity['version']}."
|
| 85 |
+
if re.search(r"quem.*criou|who made you", lowered):
|
| 86 |
+
return f"Fui criada pela {identity['created_by']}."
|
| 87 |
+
if re.search(r"qual.*seu nome", lowered):
|
| 88 |
+
return f"Meu nome é {identity['name']}."
|
| 89 |
+
return (
|
| 90 |
+
f"Eu sou {identity['name']} {identity['version']}, criada pela "
|
| 91 |
+
f"{identity['created_by']}. Meu propósito é ser um "
|
| 92 |
+
f"{identity['purpose'].lower()}."
|
| 93 |
+
)
|
delta/model.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PyTorch implementation of the Delta Ultra Mini decoder-only Transformer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import math
|
| 8 |
+
import os
|
| 9 |
+
from dataclasses import asdict, dataclass
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from torch import nn
|
| 15 |
+
from torch.nn import functional as F
|
| 16 |
+
|
| 17 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(slots=True)
|
| 22 |
+
class DeltaConfig:
|
| 23 |
+
"""Configuration for Delta Ultra Mini.
|
| 24 |
+
|
| 25 |
+
Attributes:
|
| 26 |
+
vocab_size: Token vocabulary size.
|
| 27 |
+
d_model: Embedding and hidden size.
|
| 28 |
+
n_heads: Number of attention heads.
|
| 29 |
+
n_layers: Number of decoder blocks.
|
| 30 |
+
d_ff: Feed-forward hidden size.
|
| 31 |
+
max_seq_len: Maximum context length.
|
| 32 |
+
dropout: Dropout probability.
|
| 33 |
+
tie_embeddings: Whether output projection shares token embedding weight.
|
| 34 |
+
pad_token_id: Padding token id.
|
| 35 |
+
bos_token_id: Beginning-of-sequence token id.
|
| 36 |
+
eos_token_id: End-of-sequence token id.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
vocab_size: int = 32000
|
| 40 |
+
d_model: int = 512
|
| 41 |
+
n_heads: int = 8
|
| 42 |
+
n_layers: int = 8
|
| 43 |
+
d_ff: int = 2048
|
| 44 |
+
max_seq_len: int = 512
|
| 45 |
+
dropout: float = 0.1
|
| 46 |
+
tie_embeddings: bool = True
|
| 47 |
+
pad_token_id: int = 0
|
| 48 |
+
bos_token_id: int = 2
|
| 49 |
+
eos_token_id: int = 3
|
| 50 |
+
use_cache: bool = True
|
| 51 |
+
|
| 52 |
+
@classmethod
|
| 53 |
+
def from_dict(cls, data: dict[str, Any]) -> "DeltaConfig":
|
| 54 |
+
"""Build a config from a dictionary."""
|
| 55 |
+
|
| 56 |
+
valid = {field for field in cls.__dataclass_fields__}
|
| 57 |
+
return cls(**{key: value for key, value in data.items() if key in valid})
|
| 58 |
+
|
| 59 |
+
@classmethod
|
| 60 |
+
def from_json(cls, path: str | Path) -> "DeltaConfig":
|
| 61 |
+
"""Load a config from a JSON file."""
|
| 62 |
+
|
| 63 |
+
with Path(path).open("r", encoding="utf-8") as handle:
|
| 64 |
+
return cls.from_dict(json.load(handle))
|
| 65 |
+
|
| 66 |
+
def to_dict(self) -> dict[str, Any]:
|
| 67 |
+
"""Serialize config to a dictionary."""
|
| 68 |
+
|
| 69 |
+
return asdict(self)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class RMSNorm(nn.Module):
|
| 73 |
+
"""Root Mean Square normalization without mean-centering."""
|
| 74 |
+
|
| 75 |
+
def __init__(self, dim: int, eps: float = 1e-6) -> None:
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.eps = eps
|
| 78 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 79 |
+
|
| 80 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 81 |
+
"""Normalize the last dimension of x."""
|
| 82 |
+
|
| 83 |
+
normed = x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
| 84 |
+
return normed * self.weight
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class RotaryEmbedding(nn.Module):
|
| 88 |
+
"""Rotary positional embedding cache for attention heads."""
|
| 89 |
+
|
| 90 |
+
def __init__(self, dim: int, max_seq_len: int = 512, base: float = 10000.0) -> None:
|
| 91 |
+
super().__init__()
|
| 92 |
+
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
|
| 93 |
+
positions = torch.arange(max_seq_len, dtype=torch.float)
|
| 94 |
+
freqs = torch.outer(positions, inv_freq)
|
| 95 |
+
emb = torch.cat((freqs, freqs), dim=-1)
|
| 96 |
+
self.register_buffer("cos_cached", emb.cos()[None, None, :, :], persistent=False)
|
| 97 |
+
self.register_buffer("sin_cached", emb.sin()[None, None, :, :], persistent=False)
|
| 98 |
+
|
| 99 |
+
def forward(self, seq_len: int, offset: int = 0) -> tuple[torch.Tensor, torch.Tensor]:
|
| 100 |
+
"""Return cosine and sine caches for a sequence span."""
|
| 101 |
+
|
| 102 |
+
end = offset + seq_len
|
| 103 |
+
return self.cos_cached[:, :, offset:end, :], self.sin_cached[:, :, offset:end, :]
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 107 |
+
"""Rotate pairs of hidden dimensions for RoPE."""
|
| 108 |
+
|
| 109 |
+
x1, x2 = x.chunk(2, dim=-1)
|
| 110 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def apply_rotary(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
|
| 114 |
+
"""Apply rotary embedding to q or k tensors."""
|
| 115 |
+
|
| 116 |
+
return (x * cos) + (_rotate_half(x) * sin)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class CausalSelfAttention(nn.Module):
|
| 120 |
+
"""Multi-head causal self-attention with optional KV cache."""
|
| 121 |
+
|
| 122 |
+
def __init__(self, config: DeltaConfig) -> None:
|
| 123 |
+
super().__init__()
|
| 124 |
+
if config.d_model % config.n_heads != 0:
|
| 125 |
+
raise ValueError("d_model must be divisible by n_heads")
|
| 126 |
+
self.n_heads = config.n_heads
|
| 127 |
+
self.head_dim = config.d_model // config.n_heads
|
| 128 |
+
self.qkv_proj = nn.Linear(config.d_model, 3 * config.d_model, bias=False)
|
| 129 |
+
self.out_proj = nn.Linear(config.d_model, config.d_model, bias=False)
|
| 130 |
+
self.dropout = nn.Dropout(config.dropout)
|
| 131 |
+
self.rope = RotaryEmbedding(self.head_dim, config.max_seq_len)
|
| 132 |
+
mask = torch.tril(torch.ones(config.max_seq_len, config.max_seq_len, dtype=torch.bool))
|
| 133 |
+
self.register_buffer("causal_mask", mask, persistent=False)
|
| 134 |
+
|
| 135 |
+
def forward(
|
| 136 |
+
self,
|
| 137 |
+
x: torch.Tensor,
|
| 138 |
+
past_key_value: tuple[torch.Tensor, torch.Tensor] | None = None,
|
| 139 |
+
use_cache: bool = False,
|
| 140 |
+
) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
|
| 141 |
+
"""Run attention.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
x: Input tensor of shape (batch, seq, hidden).
|
| 145 |
+
past_key_value: Optional cached key and value tensors.
|
| 146 |
+
use_cache: Whether to return a new cache.
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
Attention output and optional key/value cache.
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
batch_size, seq_len, hidden_size = x.shape
|
| 153 |
+
qkv = self.qkv_proj(x)
|
| 154 |
+
q, k, v = qkv.split(hidden_size, dim=-1)
|
| 155 |
+
q = q.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
|
| 156 |
+
k = k.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
|
| 157 |
+
v = v.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
|
| 158 |
+
|
| 159 |
+
past_len = 0 if past_key_value is None else past_key_value[0].size(2)
|
| 160 |
+
cos, sin = self.rope(seq_len, offset=past_len)
|
| 161 |
+
q = apply_rotary(q, cos.to(q.device, q.dtype), sin.to(q.device, q.dtype))
|
| 162 |
+
k = apply_rotary(k, cos.to(k.device, k.dtype), sin.to(k.device, k.dtype))
|
| 163 |
+
|
| 164 |
+
if past_key_value is not None:
|
| 165 |
+
past_k, past_v = past_key_value
|
| 166 |
+
k = torch.cat((past_k, k), dim=2)
|
| 167 |
+
v = torch.cat((past_v, v), dim=2)
|
| 168 |
+
|
| 169 |
+
present = (k, v) if use_cache else None
|
| 170 |
+
attn_scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
|
| 171 |
+
total_len = k.size(2)
|
| 172 |
+
if past_len == 0:
|
| 173 |
+
mask = self.causal_mask[:seq_len, :total_len]
|
| 174 |
+
attn_scores = attn_scores.masked_fill(~mask[None, None, :, :], torch.finfo(attn_scores.dtype).min)
|
| 175 |
+
attn_weights = F.softmax(attn_scores, dim=-1)
|
| 176 |
+
attn_weights = self.dropout(attn_weights)
|
| 177 |
+
y = torch.matmul(attn_weights, v)
|
| 178 |
+
y = y.transpose(1, 2).contiguous().view(batch_size, seq_len, hidden_size)
|
| 179 |
+
return self.out_proj(y), present
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class SwiGLUFeedForward(nn.Module):
|
| 183 |
+
"""SwiGLU feed-forward network."""
|
| 184 |
+
|
| 185 |
+
def __init__(self, config: DeltaConfig) -> None:
|
| 186 |
+
super().__init__()
|
| 187 |
+
self.gate_proj = nn.Linear(config.d_model, config.d_ff, bias=False)
|
| 188 |
+
self.up_proj = nn.Linear(config.d_model, config.d_ff, bias=False)
|
| 189 |
+
self.down_proj = nn.Linear(config.d_ff, config.d_model, bias=False)
|
| 190 |
+
self.dropout = nn.Dropout(config.dropout)
|
| 191 |
+
|
| 192 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 193 |
+
"""Apply SwiGLU transformation."""
|
| 194 |
+
|
| 195 |
+
return self.down_proj(self.dropout(F.silu(self.gate_proj(x)) * self.up_proj(x)))
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class DeltaDecoderBlock(nn.Module):
|
| 199 |
+
"""One Delta decoder block: RMSNorm, attention, RMSNorm, SwiGLU FFN."""
|
| 200 |
+
|
| 201 |
+
def __init__(self, config: DeltaConfig) -> None:
|
| 202 |
+
super().__init__()
|
| 203 |
+
self.attn_norm = RMSNorm(config.d_model)
|
| 204 |
+
self.attn = CausalSelfAttention(config)
|
| 205 |
+
self.ffn_norm = RMSNorm(config.d_model)
|
| 206 |
+
self.ffn = SwiGLUFeedForward(config)
|
| 207 |
+
|
| 208 |
+
def forward(
|
| 209 |
+
self,
|
| 210 |
+
x: torch.Tensor,
|
| 211 |
+
past_key_value: tuple[torch.Tensor, torch.Tensor] | None = None,
|
| 212 |
+
use_cache: bool = False,
|
| 213 |
+
) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
|
| 214 |
+
"""Run one decoder block."""
|
| 215 |
+
|
| 216 |
+
attn_out, present = self.attn(self.attn_norm(x), past_key_value=past_key_value, use_cache=use_cache)
|
| 217 |
+
x = x + attn_out
|
| 218 |
+
x = x + self.ffn(self.ffn_norm(x))
|
| 219 |
+
return x, present
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
class DeltaModel(nn.Module):
|
| 223 |
+
"""Delta Ultra Mini causal language model."""
|
| 224 |
+
|
| 225 |
+
def __init__(self, config: DeltaConfig | dict[str, Any] | None = None) -> None:
|
| 226 |
+
super().__init__()
|
| 227 |
+
self.config = DeltaConfig.from_dict(config) if isinstance(config, dict) else (config or DeltaConfig())
|
| 228 |
+
self.embed_tokens = nn.Embedding(self.config.vocab_size, self.config.d_model)
|
| 229 |
+
self.drop = nn.Dropout(self.config.dropout)
|
| 230 |
+
self.layers = nn.ModuleList(DeltaDecoderBlock(self.config) for _ in range(self.config.n_layers))
|
| 231 |
+
self.norm = RMSNorm(self.config.d_model)
|
| 232 |
+
self.lm_head = nn.Linear(self.config.d_model, self.config.vocab_size, bias=False)
|
| 233 |
+
if self.config.tie_embeddings:
|
| 234 |
+
self.lm_head.weight = self.embed_tokens.weight
|
| 235 |
+
self.apply(self._init_weights)
|
| 236 |
+
total_params = self.num_parameters()
|
| 237 |
+
logger.info("DeltaModel initialized with %s parameters", f"{total_params:,}")
|
| 238 |
+
print(f"DeltaModel parameters: {total_params:,}")
|
| 239 |
+
if not 48_000_000 <= total_params <= 55_000_000:
|
| 240 |
+
raise ValueError(f"Delta Ultra Mini must have 48M-55M parameters, got {total_params:,}")
|
| 241 |
+
|
| 242 |
+
def _init_weights(self, module: nn.Module) -> None:
|
| 243 |
+
"""Initialize weights with GPT-style normal initialization."""
|
| 244 |
+
|
| 245 |
+
if isinstance(module, nn.Linear):
|
| 246 |
+
nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
| 247 |
+
if module.bias is not None:
|
| 248 |
+
nn.init.zeros_(module.bias)
|
| 249 |
+
elif isinstance(module, nn.Embedding):
|
| 250 |
+
nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
| 251 |
+
|
| 252 |
+
def num_parameters(self, only_trainable: bool = True, exclude_embeddings: bool = False) -> int:
|
| 253 |
+
"""Return the number of model parameters.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
only_trainable: Count only parameters with requires_grad.
|
| 257 |
+
exclude_embeddings: Exclude embedding parameters for Trainer FLOPs estimates.
|
| 258 |
+
"""
|
| 259 |
+
|
| 260 |
+
total = 0
|
| 261 |
+
for name, parameter in self.named_parameters():
|
| 262 |
+
if only_trainable and not parameter.requires_grad:
|
| 263 |
+
continue
|
| 264 |
+
if exclude_embeddings and "embed_tokens" in name:
|
| 265 |
+
continue
|
| 266 |
+
total += parameter.numel()
|
| 267 |
+
return total
|
| 268 |
+
|
| 269 |
+
def forward(
|
| 270 |
+
self,
|
| 271 |
+
input_ids: torch.Tensor,
|
| 272 |
+
labels: torch.Tensor | None = None,
|
| 273 |
+
past_key_values: list[tuple[torch.Tensor, torch.Tensor]] | None = None,
|
| 274 |
+
use_cache: bool = False,
|
| 275 |
+
**_: Any,
|
| 276 |
+
) -> dict[str, torch.Tensor | list[tuple[torch.Tensor, torch.Tensor]] | None]:
|
| 277 |
+
"""Run causal language modeling forward pass."""
|
| 278 |
+
|
| 279 |
+
if input_ids.size(1) > self.config.max_seq_len:
|
| 280 |
+
input_ids = input_ids[:, -self.config.max_seq_len :]
|
| 281 |
+
if labels is not None:
|
| 282 |
+
labels = labels[:, -self.config.max_seq_len :]
|
| 283 |
+
x = self.drop(self.embed_tokens(input_ids))
|
| 284 |
+
next_cache: list[tuple[torch.Tensor, torch.Tensor]] = []
|
| 285 |
+
for index, layer in enumerate(self.layers):
|
| 286 |
+
past = None if past_key_values is None else past_key_values[index]
|
| 287 |
+
x, present = layer(x, past_key_value=past, use_cache=use_cache)
|
| 288 |
+
if present is not None:
|
| 289 |
+
next_cache.append(present)
|
| 290 |
+
logits = self.lm_head(self.norm(x))
|
| 291 |
+
loss = None
|
| 292 |
+
if labels is not None:
|
| 293 |
+
shift_logits = logits[:, :-1, :].contiguous()
|
| 294 |
+
shift_labels = labels[:, 1:].contiguous()
|
| 295 |
+
loss = F.cross_entropy(
|
| 296 |
+
shift_logits.view(-1, shift_logits.size(-1)),
|
| 297 |
+
shift_labels.view(-1),
|
| 298 |
+
ignore_index=-100,
|
| 299 |
+
)
|
| 300 |
+
return {"loss": loss, "logits": logits, "past_key_values": next_cache if use_cache else None}
|
| 301 |
+
|
| 302 |
+
def save_checkpoint(
|
| 303 |
+
self,
|
| 304 |
+
path: str | Path,
|
| 305 |
+
optimizer: torch.optim.Optimizer | None = None,
|
| 306 |
+
scheduler: Any | None = None,
|
| 307 |
+
step: int = 0,
|
| 308 |
+
) -> None:
|
| 309 |
+
"""Save a full training checkpoint."""
|
| 310 |
+
|
| 311 |
+
checkpoint: dict[str, Any] = {
|
| 312 |
+
"model_state_dict": self.state_dict(),
|
| 313 |
+
"step": step,
|
| 314 |
+
"config": self.config.to_dict(),
|
| 315 |
+
}
|
| 316 |
+
if optimizer is not None:
|
| 317 |
+
checkpoint["optimizer_state_dict"] = optimizer.state_dict()
|
| 318 |
+
if scheduler is not None:
|
| 319 |
+
checkpoint["scheduler_state_dict"] = scheduler.state_dict()
|
| 320 |
+
path = Path(path)
|
| 321 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 322 |
+
torch.save(checkpoint, path)
|
| 323 |
+
|
| 324 |
+
@classmethod
|
| 325 |
+
def load_checkpoint(cls, path: str | Path, map_location: str | torch.device = "cpu") -> "DeltaModel":
|
| 326 |
+
"""Load a model from a checkpoint file."""
|
| 327 |
+
|
| 328 |
+
checkpoint = torch.load(path, map_location=map_location)
|
| 329 |
+
model = cls(DeltaConfig.from_dict(checkpoint["config"]))
|
| 330 |
+
model.load_state_dict(checkpoint["model_state_dict"])
|
| 331 |
+
return model
|
delta/tokenizer.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BPE tokenizer training and chat formatting for Delta Ultra Mini."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
from tokenizers import Tokenizer
|
| 11 |
+
from tokenizers.decoders import ByteLevel as ByteLevelDecoder
|
| 12 |
+
from tokenizers.models import BPE
|
| 13 |
+
from tokenizers.pre_tokenizers import ByteLevel
|
| 14 |
+
from tokenizers.processors import TemplateProcessing
|
| 15 |
+
from tokenizers.trainers import BpeTrainer
|
| 16 |
+
|
| 17 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
SPECIAL_TOKENS: list[str] = ["[PAD]", "[UNK]", "[BOS]", "[EOS]", "[SYS]", "[USR]", "[ASS]", "[SEP]"]
|
| 21 |
+
DEFAULT_SYSTEM_PROMPT = (
|
| 22 |
+
"Você é Delta, assistente criada pela Flame Corporation. "
|
| 23 |
+
"Responda de forma clara, útil e amigável."
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def train_tokenizer(corpus_files: list[str] | list[Path], output_path: str | Path) -> None:
|
| 28 |
+
"""Train a BPE tokenizer from raw text files.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
corpus_files: Paths to corpus files.
|
| 32 |
+
output_path: Destination tokenizer JSON path.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
|
| 36 |
+
tokenizer.pre_tokenizer = ByteLevel(add_prefix_space=False)
|
| 37 |
+
tokenizer.decoder = ByteLevelDecoder()
|
| 38 |
+
trainer = BpeTrainer(vocab_size=32000, special_tokens=SPECIAL_TOKENS, show_progress=True)
|
| 39 |
+
tokenizer.train([str(path) for path in corpus_files], trainer)
|
| 40 |
+
tokenizer.post_processor = TemplateProcessing(
|
| 41 |
+
single="[BOS] $A [EOS]",
|
| 42 |
+
pair="[BOS] $A [SEP] $B [EOS]",
|
| 43 |
+
special_tokens=[
|
| 44 |
+
("[BOS]", tokenizer.token_to_id("[BOS]")),
|
| 45 |
+
("[EOS]", tokenizer.token_to_id("[EOS]")),
|
| 46 |
+
("[SEP]", tokenizer.token_to_id("[SEP]")),
|
| 47 |
+
],
|
| 48 |
+
)
|
| 49 |
+
output = Path(output_path)
|
| 50 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
tokenizer.save(str(output))
|
| 52 |
+
logger.info("Tokenizer saved to %s", output)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def load_tokenizer(path: str | Path) -> "DeltaTokenizer":
|
| 56 |
+
"""Load a DeltaTokenizer from disk."""
|
| 57 |
+
|
| 58 |
+
return DeltaTokenizer(path)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class DeltaTokenizer:
|
| 62 |
+
"""Thin wrapper around HuggingFace tokenizers.Tokenizer."""
|
| 63 |
+
|
| 64 |
+
def __init__(self, path: str | Path) -> None:
|
| 65 |
+
self.path = Path(path)
|
| 66 |
+
self.tokenizer = Tokenizer.from_file(str(self.path))
|
| 67 |
+
self.pad_token_id = self.tokenizer.token_to_id("[PAD]")
|
| 68 |
+
self.unk_token_id = self.tokenizer.token_to_id("[UNK]")
|
| 69 |
+
self.bos_token_id = self.tokenizer.token_to_id("[BOS]")
|
| 70 |
+
self.eos_token_id = self.tokenizer.token_to_id("[EOS]")
|
| 71 |
+
self.sys_token_id = self.tokenizer.token_to_id("[SYS]")
|
| 72 |
+
self.usr_token_id = self.tokenizer.token_to_id("[USR]")
|
| 73 |
+
self.ass_token_id = self.tokenizer.token_to_id("[ASS]")
|
| 74 |
+
self.sep_token_id = self.tokenizer.token_to_id("[SEP]")
|
| 75 |
+
self.special_tokens = set(SPECIAL_TOKENS)
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def chat_stop_token_ids(self) -> set[int]:
|
| 79 |
+
"""Token ids that should end an assistant completion."""
|
| 80 |
+
|
| 81 |
+
return {
|
| 82 |
+
token_id
|
| 83 |
+
for token_id in (
|
| 84 |
+
self.eos_token_id,
|
| 85 |
+
self.sep_token_id,
|
| 86 |
+
self.sys_token_id,
|
| 87 |
+
self.usr_token_id,
|
| 88 |
+
self.ass_token_id,
|
| 89 |
+
self.bos_token_id,
|
| 90 |
+
self.pad_token_id,
|
| 91 |
+
)
|
| 92 |
+
if token_id is not None
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
def encode(self, text: str, add_special_tokens: bool = True) -> list[int]:
|
| 96 |
+
"""Encode a string into token ids."""
|
| 97 |
+
|
| 98 |
+
return self.tokenizer.encode(text, add_special_tokens=add_special_tokens).ids
|
| 99 |
+
|
| 100 |
+
def decode(self, ids: list[int], skip_special_tokens: bool = True) -> str:
|
| 101 |
+
"""Decode token ids into text."""
|
| 102 |
+
|
| 103 |
+
return self.tokenizer.decode(ids, skip_special_tokens=skip_special_tokens)
|
| 104 |
+
|
| 105 |
+
def batch_encode(self, texts: list[str], add_special_tokens: bool = True) -> list[list[int]]:
|
| 106 |
+
"""Encode a batch of strings."""
|
| 107 |
+
|
| 108 |
+
return [encoding.ids for encoding in self.tokenizer.encode_batch(texts, add_special_tokens=add_special_tokens)]
|
| 109 |
+
|
| 110 |
+
def format_chat(self, messages: list[dict[str, Any]], persona: str | None = None) -> str:
|
| 111 |
+
"""Format a multi-turn conversation for Delta.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
messages: Conversation turns with role and content.
|
| 115 |
+
persona: Optional system prompt.
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
Prompt text ending with an assistant tag for continuation.
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
system = persona or DEFAULT_SYSTEM_PROMPT
|
| 122 |
+
parts = [f"[SYS] {system} [SEP]"]
|
| 123 |
+
for message in messages:
|
| 124 |
+
role = str(message.get("role", "")).lower()
|
| 125 |
+
content = str(message.get("content", "")).strip()
|
| 126 |
+
if role == "user":
|
| 127 |
+
parts.append(f"[USR] {content} [SEP]")
|
| 128 |
+
elif role == "assistant":
|
| 129 |
+
parts.append(f"[ASS] {content} [SEP]")
|
| 130 |
+
elif role == "system":
|
| 131 |
+
parts[0] = f"[SYS] {content} [SEP]"
|
| 132 |
+
if not parts[-1].startswith("[ASS]"):
|
| 133 |
+
parts.append("[ASS]")
|
| 134 |
+
else:
|
| 135 |
+
parts.append("[USR] [SEP]")
|
| 136 |
+
parts.append("[ASS]")
|
| 137 |
+
return "\n".join(parts)
|
delta/trainer.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training utilities for Delta Ultra Mini using HuggingFace Trainer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from transformers import Trainer, TrainingArguments
|
| 13 |
+
from transformers.utils import SAFE_WEIGHTS_NAME
|
| 14 |
+
|
| 15 |
+
from delta.dataset import DeltaDataCollator, DeltaDataset
|
| 16 |
+
from delta.model import DeltaConfig, DeltaModel
|
| 17 |
+
from delta.tokenizer import load_tokenizer
|
| 18 |
+
|
| 19 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class DeltaTrainer(Trainer):
|
| 24 |
+
"""Trainer override that saves tied weights safely with safetensors."""
|
| 25 |
+
|
| 26 |
+
def _save(self, output_dir: str | None = None, state_dict: dict[str, torch.Tensor] | None = None) -> None:
|
| 27 |
+
output_dir = output_dir or self.args.output_dir
|
| 28 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
state_dict = state_dict or self.model.state_dict()
|
| 31 |
+
weights_path = os.path.join(output_dir, SAFE_WEIGHTS_NAME)
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
import safetensors.torch
|
| 35 |
+
|
| 36 |
+
safetensors.torch.save_file(state_dict, weights_path, metadata={"format": "pt"})
|
| 37 |
+
except RuntimeError as exc:
|
| 38 |
+
# When embeddings are tied, tensors share the same underlying storage.
|
| 39 |
+
# `save_file(state_dict, ...)` refuses that; `save_model(model, ...)` handles it.
|
| 40 |
+
if "share memory" not in str(exc):
|
| 41 |
+
raise
|
| 42 |
+
import safetensors.torch
|
| 43 |
+
|
| 44 |
+
safetensors.torch.save_model(self.model, weights_path, metadata={"format": "pt"})
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def train(config_dict: dict[str, Any]) -> Trainer:
|
| 48 |
+
"""Train Delta Ultra Mini.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
config_dict: Training and model configuration.
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
The configured Trainer after training.
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
data_path = Path(config_dict["data_path"])
|
| 58 |
+
output_dir = Path(config_dict["output_dir"])
|
| 59 |
+
tokenizer_path = Path(config_dict.get("tokenizer_path", output_dir / "tokenizer.json"))
|
| 60 |
+
epochs = float(config_dict.get("epochs", 1))
|
| 61 |
+
batch_size = int(config_dict.get("batch_size", 2))
|
| 62 |
+
resume_from_checkpoint = config_dict.get("resume_from_checkpoint")
|
| 63 |
+
model_config = DeltaConfig.from_dict(config_dict.get("model", config_dict))
|
| 64 |
+
tokenizer = load_tokenizer(tokenizer_path)
|
| 65 |
+
dataset = DeltaDataset(
|
| 66 |
+
data_path=data_path,
|
| 67 |
+
tokenizer=tokenizer,
|
| 68 |
+
max_seq_len=model_config.max_seq_len,
|
| 69 |
+
stride=int(config_dict.get("stride", 256)),
|
| 70 |
+
)
|
| 71 |
+
model = DeltaModel(model_config)
|
| 72 |
+
training_args = TrainingArguments(
|
| 73 |
+
output_dir=str(output_dir),
|
| 74 |
+
num_train_epochs=epochs,
|
| 75 |
+
per_device_train_batch_size=batch_size,
|
| 76 |
+
learning_rate=float(config_dict.get("learning_rate", 3e-4)),
|
| 77 |
+
weight_decay=float(config_dict.get("weight_decay", 0.01)),
|
| 78 |
+
warmup_steps=int(config_dict.get("warmup_steps", 500)),
|
| 79 |
+
lr_scheduler_type="cosine",
|
| 80 |
+
max_grad_norm=float(config_dict.get("max_grad_norm", 1.0)),
|
| 81 |
+
fp16=torch.cuda.is_available(),
|
| 82 |
+
save_steps=int(config_dict.get("save_steps", 1000)),
|
| 83 |
+
logging_steps=int(config_dict.get("logging_steps", 50)),
|
| 84 |
+
report_to=[],
|
| 85 |
+
remove_unused_columns=False,
|
| 86 |
+
dataloader_pin_memory=torch.cuda.is_available(),
|
| 87 |
+
)
|
| 88 |
+
trainer = DeltaTrainer(
|
| 89 |
+
model=model,
|
| 90 |
+
args=training_args,
|
| 91 |
+
train_dataset=dataset,
|
| 92 |
+
data_collator=DeltaDataCollator(tokenizer.pad_token_id),
|
| 93 |
+
)
|
| 94 |
+
trainer.train(resume_from_checkpoint=resume_from_checkpoint)
|
| 95 |
+
trainer.save_model(str(output_dir))
|
| 96 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 97 |
+
model.save_checkpoint(
|
| 98 |
+
output_dir / "delta_checkpoint.pt",
|
| 99 |
+
optimizer=trainer.optimizer,
|
| 100 |
+
scheduler=trainer.lr_scheduler,
|
| 101 |
+
step=int(trainer.state.global_step),
|
| 102 |
+
)
|
| 103 |
+
with (output_dir / "config.json").open("w", encoding="utf-8") as handle:
|
| 104 |
+
json.dump(model_config.to_dict(), handle, ensure_ascii=False, indent=2)
|
| 105 |
+
logger.info("Saved Delta checkpoint to %s", output_dir / "delta_checkpoint.pt")
|
| 106 |
+
return trainer
|
delta_checkpoint.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ff56db700e129603ddb8d75ed058a2aa6d707c59f176e643238913229e2ee10
|
| 3 |
+
size 599439053
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37ae14167b33ca2943e5a665a2e04acc6d7718694cff13900544e1b1fd42ac4f
|
| 3 |
+
size 199794416
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
tokenizers
|
| 3 |
+
transformers
|
| 4 |
+
accelerate>=1.1.0
|
scripts/generate_delta.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command line local inference for Delta Ultra Mini without the REST API."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 14 |
+
|
| 15 |
+
from delta.generator import DeltaGenerator
|
| 16 |
+
|
| 17 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def parse_args() -> argparse.Namespace:
|
| 22 |
+
"""Parse command line arguments."""
|
| 23 |
+
|
| 24 |
+
parser = argparse.ArgumentParser(description="Generate text with Delta Ultra Mini.")
|
| 25 |
+
parser.add_argument("--prompt", required=True, help="User prompt.")
|
| 26 |
+
parser.add_argument("--checkpoint_path", default="runs/delta-ultra-mini/delta_checkpoint.pt")
|
| 27 |
+
parser.add_argument("--tokenizer_path", default="tokenizer.json")
|
| 28 |
+
parser.add_argument("--config_path", default="configs/ultra_mini.json")
|
| 29 |
+
parser.add_argument("--max_new_tokens", type=int, default=128)
|
| 30 |
+
parser.add_argument("--temperature", type=float, default=0.2)
|
| 31 |
+
parser.add_argument("--top_k", type=int, default=20)
|
| 32 |
+
parser.add_argument("--top_p", type=float, default=0.9)
|
| 33 |
+
parser.add_argument("--repetition_penalty", type=float, default=1.08)
|
| 34 |
+
return parser.parse_args()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def main() -> None:
|
| 38 |
+
"""Load the checkpoint and print one assistant response."""
|
| 39 |
+
|
| 40 |
+
args = parse_args()
|
| 41 |
+
generator = DeltaGenerator.from_files(
|
| 42 |
+
checkpoint_path=args.checkpoint_path,
|
| 43 |
+
tokenizer_path=args.tokenizer_path,
|
| 44 |
+
config_path=args.config_path,
|
| 45 |
+
)
|
| 46 |
+
response = generator.chat(
|
| 47 |
+
[{"role": "user", "content": args.prompt}],
|
| 48 |
+
max_new_tokens=args.max_new_tokens,
|
| 49 |
+
temperature=args.temperature,
|
| 50 |
+
top_k=args.top_k,
|
| 51 |
+
top_p=args.top_p,
|
| 52 |
+
repetition_penalty=args.repetition_penalty,
|
| 53 |
+
)
|
| 54 |
+
print(response)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
main()
|
scripts/train_delta.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command line training entrypoint for Delta Ultra Mini."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 16 |
+
|
| 17 |
+
from delta.trainer import train
|
| 18 |
+
|
| 19 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def parse_args() -> argparse.Namespace:
|
| 24 |
+
"""Parse training arguments."""
|
| 25 |
+
|
| 26 |
+
parser = argparse.ArgumentParser(description="Train Delta Ultra Mini.")
|
| 27 |
+
parser.add_argument("--data_path", required=True, help="Directory or file containing .txt/.jsonl training data.")
|
| 28 |
+
parser.add_argument("--output_dir", required=True, help="Output directory for checkpoints.")
|
| 29 |
+
parser.add_argument("--epochs", type=float, default=1.0, help="Number of training epochs.")
|
| 30 |
+
parser.add_argument("--batch_size", type=int, default=2, help="Per-device train batch size.")
|
| 31 |
+
parser.add_argument("--resume_from_checkpoint", default=None, help="Checkpoint path or true to resume latest.")
|
| 32 |
+
parser.add_argument("--tokenizer_path", default=None, help="Path to tokenizer.json.")
|
| 33 |
+
parser.add_argument("--config_path", default="configs/ultra_mini.json", help="Model config JSON.")
|
| 34 |
+
return parser.parse_args()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def main() -> None:
|
| 38 |
+
"""Run Trainer-based model training."""
|
| 39 |
+
|
| 40 |
+
args = parse_args()
|
| 41 |
+
with Path(args.config_path).open("r", encoding="utf-8") as handle:
|
| 42 |
+
model_config: dict[str, Any] = json.load(handle)
|
| 43 |
+
output_dir = Path(args.output_dir)
|
| 44 |
+
config = {
|
| 45 |
+
"data_path": args.data_path,
|
| 46 |
+
"output_dir": str(output_dir),
|
| 47 |
+
"epochs": args.epochs,
|
| 48 |
+
"batch_size": args.batch_size,
|
| 49 |
+
"resume_from_checkpoint": args.resume_from_checkpoint,
|
| 50 |
+
"tokenizer_path": args.tokenizer_path or str(output_dir / "tokenizer.json"),
|
| 51 |
+
"model": model_config,
|
| 52 |
+
}
|
| 53 |
+
train(config)
|
| 54 |
+
logger.info("Training complete.")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
main()
|
scripts/train_tokenizer.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command line tokenizer trainer for Delta Ultra Mini."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 14 |
+
|
| 15 |
+
from delta.tokenizer import train_tokenizer
|
| 16 |
+
|
| 17 |
+
logging.basicConfig(level=os.getenv("DELTA_LOG_LEVEL", "INFO").upper())
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def parse_args() -> argparse.Namespace:
|
| 22 |
+
"""Parse command line arguments."""
|
| 23 |
+
|
| 24 |
+
parser = argparse.ArgumentParser(description="Train Delta BPE tokenizer.")
|
| 25 |
+
parser.add_argument("--corpus_files", nargs="+", required=True, help="Input .txt corpus files.")
|
| 26 |
+
parser.add_argument("--output_path", required=True, help="Output tokenizer JSON path.")
|
| 27 |
+
return parser.parse_args()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main() -> None:
|
| 31 |
+
"""Train and save a tokenizer."""
|
| 32 |
+
|
| 33 |
+
args = parse_args()
|
| 34 |
+
train_tokenizer([Path(path) for path in args.corpus_files], Path(args.output_path))
|
| 35 |
+
logger.info("Tokenizer training complete.")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == "__main__":
|
| 39 |
+
main()
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|