chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:19:01 +08:00
commit 3b90d1192f
2172 changed files with 594509 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
"""
---
title: Utilities and Helpers
summary: >
Utilities and helper functions
---
# Utilities and Helpers
* [Cache for intermediate activations (for faster inference)](cache.html)
* [Tools for finetuning](finetune.html)
* [Trainer](trainer.html)
* [Text dataset](text_dataset.html)
"""
import typing
from typing import List, Optional
import torch
from labml import logger
from labml.logger import Text
from labml_nn.neox.tokenizer import get_tokenizer
if typing.TYPE_CHECKING:
from tokenizers import Tokenizer
# Tokenizer singleton
_TOKENIZER: Optional['Tokenizer'] = None
def get_tokens(text: str) -> List[int]:
"""
### Get token ids
:param text: is the text to tokenize
:return: the token ids
"""
global _TOKENIZER
if _TOKENIZER is None:
_TOKENIZER = get_tokenizer()
return _TOKENIZER.encode_batch([text])[0].ids
def print_token_outputs(ids: List[int], *xs: torch.Tensor):
"""
### Print tokens from model outputs
Pretty prints target tokens along side outputs from the model(s).
:param ids: are the target token ids
:param xs: are the model(s) outputs
"""
ids = ids + [-1]
xs = [[-1] + x[0].max(dim=-1)[1].tolist() for x in xs]
print_tokens(ids, xs)
def print_tokens(target: List[int], others: List[List[int]]):
"""
### Print tokens
Pretty prints tokens for comparison
:param target: are the target token ids
:param others: are the sampled outputs from the model(s)
"""
# Load tokenizer
global _TOKENIZER
if _TOKENIZER is None:
_TOKENIZER = get_tokenizer()
# Convert the tokens to list of strings
text = []
for i in range(len(target)):
tokens = [_TOKENIZER.decode([target[i]]) if target[i] != -1 else '---']
for j in range(len(others)):
tokens.append(_TOKENIZER.decode([others[j][i]]) if others[j][i] != -1 else '---')
text.append(tokens)
# Stats
correct = [0 for _ in others]
total = 0
# Iterate through tokens
for i in range(len(target)):
parts = [(f'{i}: ', Text.meta)]
parts += [('"', Text.subtle), (text[i][0], Text.subtle), ('"', Text.subtle), '\t']
# Empty target
if target[i] == -1:
for j in range(len(others)):
parts += [('"', Text.subtle), (text[i][j + 1], Text.subtle), ('"', Text.subtle), '\t']
logger.log(parts)
continue
# Number of tokens
total += 1
# Other outputs
for j in range(len(others)):
correct[j] += 1 if others[j][i] == target[i] else 0
parts += [('"', Text.subtle),
(text[i][j + 1], Text.success if others[j][i] == target[i] else Text.danger),
('"', Text.subtle), '\t']
logger.log(parts)
# Stats
parts = [(f'{total}', Text.highlight), '\t']
for j in range(len(others)):
parts += [(f'{correct[j]}', Text.value), '\t']
logger.log(parts)
def balance_layers_simple(n_layers: int, n_chunks: int):
"""
### Balance layers
Split the `n_layers` into `n_chunks`. This is used for pipeline parallel training.
:param n_layers: is the number of layers
:param n_chunks: is the number of chunks
:return: returns a list with the number of layers for each chunk
"""
balance = []
for i in range(n_chunks):
balance.append((n_layers - sum(balance)) // (n_chunks - i))
return list(reversed(balance))
+118
View File
@@ -0,0 +1,118 @@
"""
---
title: Cache for Intermediate Activations
summary: >
Cache for intermediate activations for faster inference.
---
# Cache for Intermediate Activations
During inference the model outputs token by token.
We use this simple cache to store key's and value's attention layers,
so that we don't have to recompute them for previous tokens.
"""
from typing import Any
class Cache:
"""
## Cache
This maintains a key-value cache and queues push values and pop them in the same order.
The queues are useful since we have multiple attention layers.
"""
def __init__(self):
self._cache = {}
def clear_all(self):
"""
### Clear cache
"""
self._cache = {}
def push(self, name: str, value: Any):
"""
### Push a value to a queue
:param name: is the name of the queue
:param value: is the value to be pushed
"""
# Create an empty queue if it's not present
if name not in self._cache:
self._cache[name] = []
# Push to the queue
self._cache[name].append(value)
def q_size(self, name):
"""
### Return the size of the queue
:param name: is the name of the queue
:return: size of the queue if exists else None
"""
if name not in self._cache:
return None
if type(self._cache[name]) != list:
return None
return len(self._cache[name])
def pop(self, name: str):
"""
### Pop from a queue
:param name: is the name of the queue
:return: the value
"""
return self._cache[name].pop(0)
def set(self, key: str, value: Any):
"""
### Cache a value
:param key: is the name of the value to be cached
:param value: is the value
"""
self._cache[key] = value
def get(self, key: str, default: Any = None):
"""
### Retrieve a value from cache
:param key: is the name used when caching
:param default: is the default value if the cache is empty
:return: the cached value
"""
return self._cache.get(key, default)
def clear(self, key: str):
"""
### Clear a cache value
:param key: is the name used when caching
"""
del self._cache[key]
# Singleton for cache
_INSTANCE = None
def get_cache() -> Cache:
"""
### Get the cache instance
:return: the cache instance
"""
global _INSTANCE
if _INSTANCE is None:
_INSTANCE = Cache()
return _INSTANCE
+55
View File
@@ -0,0 +1,55 @@
from typing import List, Dict
import torch
from torch import nn
from labml_nn.neox.model import TransformerLayer, NeoXModule
class FineTuner:
def __init__(self, layers: List[NeoXModule]):
self.layers = layers
def get_trainable_params(self) -> Dict[str, nn.Parameter]:
params = {}
for i, layer in enumerate(self.layers):
params.update(self.get_layer_trainable_params(layer, prefix=f'layer_{i :02d}'))
return params
def get_layer_trainable_params(self, layer: NeoXModule, prefix: str) -> Dict[str, nn.Parameter]:
raise NotImplementedError
def set_trainable_params(self):
for layer in self.layers:
# Set `requires_grad` to `False` for the entire layer.
layer.requires_grad_(False)
#
for p in self.get_trainable_params().values():
p.requires_grad_(True)
def state_dict(self):
return {n: p.data.cpu() for n, p in self.get_trainable_params().items()}
def load_state_dict(self, state_dict: Dict[str, torch.Tensor]):
params = self.get_trainable_params()
for n, p in params.items():
p.data[:] = state_dict[n].to(p.data.device)
for n in state_dict.keys():
assert n in params, n
class FineTuneBiases(FineTuner):
def get_layer_trainable_params(self, layer: NeoXModule, prefix: str) -> Dict[str, nn.Parameter]:
params = {}
if isinstance(layer, TransformerLayer):
# No need to train the mlp bias because we are adding it with attention output
params[f'{prefix}.attention.output.bias'] = layer.attention.output.bias
params[f'{prefix}.attention.qkv_lin.bias'] = layer.attention.qkv_lin.bias
params[f'{prefix}.ffn.dense_h_h4.bias'] = layer.ffn.dense_h_h4.bias
else:
pass
return params
+75
View File
@@ -0,0 +1,75 @@
"""
---
title: LLM.int8() on GPT-NeoX
summary: >
Transform nn.Linear layers to 8-bit integer layers.
---
# LLM.int() on GPT-NeoX
This implements a utility function to transform a `nn.Linear` layer to LLM.int8() linear layer.
[LLM.int8() paper](https://arxiv.org/abs/eb2bcaee1d0011edaa66a71c10a887e7)
shows you can use int8 quantization while handling outliers to
reduce memory footprint without performance degradation in large language models.
They convert weights and inputs to scaled 8-bit integers and does matrix multiplication
producing int32 results which is then converted back to float16 and rescaled.
They show that in large langauge models, some features can give extreme values (outliers)
that dominate the model's output.
These features get clamped in 8-bit integer space which causes the model performance to degrade.
As a solution they pick these outliers (greater than a specified threshold)
and compute their multiplications separately in float16 space.
Since the percentage of outliers is around 0.01% this doesn't increase memory usage,
and prevents the model from degrading performance.
The code to transform GPT-NoeX layers is defined in [model.py](../model.html#post_load_prepare).
Here are example uses of GPT-NeoX with int8 quantization.
* [Generate Text](../samples/llm_int8.html)
* [Run Evaluation Tests](../evaluation/llm_int8.html)
"""
# Import [`bitsandbytes`](https://github.com/timdettmers/bitsandbytes) package
try:
from bitsandbytes.nn import Linear8bitLt, Int8Params
except ImportError:
raise ImportError('''Please install `bitsandbytes` with `pip install bitsandbytes -U`''')
import torch
from torch import nn
def make_llm_int8_linear(linear_module: nn.Linear, device: torch.device, threshold: float = 6.0):
"""
## Transform a `nn.Linear` layer to LLM.int8() linear layer
:param linear_module: is the `nn.Linear` layer to transform
:param device: is the device of the model
:param threshold: is the threshold $\alpha$ to use for outlier detection
"""
#
assert isinstance(linear_module, nn.Linear)
# Create an empty Linear8bitLt module
int8_lin = Linear8bitLt(
linear_module.in_features,
linear_module.out_features,
linear_module.bias is not None,
has_fp16_weights=False,
threshold=threshold,
)
# Quantize the weights
int8_lin._parameters['weight'] = Int8Params(linear_module.weight.data.cpu(),
requires_grad=False,
has_fp16_weights=False).to(device)
# Set the bias in float16 space
if linear_module.bias is not None:
int8_lin._parameters['bias'] = nn.Parameter(linear_module.bias.data,
requires_grad=False)
#
return int8_lin
+132
View File
@@ -0,0 +1,132 @@
"""
---
title: Text Dataset for GPT-NeoX
summary: >
Loads text datasets to fine-tune GPT-NeoX
---
# Text Dataset for GPT-NeoX
"""
from pathlib import PurePath, Path
from typing import Optional, List
import torch
import torch.utils.data
from labml import lab
from labml import monit
from labml.logger import inspect
from labml.utils.download import download_file
from labml_nn.neox.tokenizer import get_tokenizer
def load_text(path: PurePath, url: Optional[str] = None, *, filter_subset: Optional[int] = None):
"""
### Load text file
:param path: is the location of the text file
:param url: is the URL to download the file from
:param filter_subset: is the number of characters to filter.
Use this during testing when trying large datasets
:return: the text content
"""
path = Path(path)
# Download if it doesn't exist
if not path.exists():
if not url:
raise FileNotFoundError(str(path))
else:
download_file(url, path)
with monit.section("Load data"):
# Load data
with open(str(path), 'r') as f:
text = f.read()
# Filter
if filter_subset:
text = text[:filter_subset]
#
return text
class NeoXDataset(torch.utils.data.Dataset):
"""
## Dataset for fine-tuning GPT-NeoX
This is not optimized to very large datasets.
"""
def __init__(self, tokens: List[int], seq_len: int):
"""
:param tokens: is the list of token ids
:param seq_len: is the sequence length of a single training sample
"""
self.seq_len = seq_len
# Number of samples
n_samples = len(tokens) // seq_len
self.n_samples = n_samples
# Truncate
tokens = tokens[:n_samples * seq_len + 1]
# Create a PyTorch tensor
self.tokens = torch.tensor(tokens)
def __len__(self):
return self.n_samples
def __getitem__(self, idx: int):
"""
### Get a sample
:param idx: is the index of the sample
:return: the input and the target
"""
offset = idx * self.seq_len
return self.tokens[offset:offset + self.seq_len], self.tokens[offset + 1:offset + 1 + self.seq_len]
DATASETS = {
'tiny_shakespeare': {
'file': 'tiny_shakespeare.txt',
'url': 'https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt'
}
}
def get_training_data(seq_len: int = 32, dataset_name: str = 'tiny_shakespeare', truncate: int = -1):
"""
### Load Dataset
:param seq_len: is the sequence length of a single training sample
:param dataset_name: is the name of the dataset
:return: the dataset
"""
ds = DATASETS[dataset_name]
# Load the content
text = load_text(lab.get_data_path() / ds['file'], ds['url'])
# Tokenize
tokenizer = get_tokenizer()
tokens = tokenizer.encode_batch([text])[0]
if truncate > 0:
token_ids = tokens.ids[:truncate * seq_len]
else:
token_ids = tokens.ids
#
return NeoXDataset(token_ids, seq_len)
def _test():
dataset = get_training_data()
inspect(tokens=len(dataset.tokens))
#
if __name__ == '__main__':
_test()
+182
View File
@@ -0,0 +1,182 @@
from typing import Optional, Set, List
import torch.nn as nn
import torch.optim
import torch.utils.data
from torch.cuda import amp
from torch.cuda.amp import GradScaler
from labml import monit, tracker
from labml.configs import BaseConfigs, option
from labml_nn.neox.utils.finetune import FineTuner
def get_trainable_params(model: nn.Module):
"""
### Get trainable parameters
:param model: is the model to train
:return: a list of parameters for training
"""
# Get all parameters
params = list(model.parameters())
# Filter parameters that require gradients
trainable_params = [p for p in params if p.requires_grad]
#
return trainable_params
class TrainerConf(BaseConfigs):
model: nn.Module
layers: List[nn.Module]
optimizer: torch.optim.Optimizer = 'Adam'
train_loader: torch.utils.data.DataLoader
valid_loader: Optional[torch.utils.data.DataLoader] = None,
device: torch.device = torch.device('cuda:0')
scaler: Optional[GradScaler] = 'Default'
is_amp: bool = True
dtype: torch.dtype = torch.float16
is_clone_layers: bool = True
loss_func: nn.Module = nn.CrossEntropyLoss()
checkpoints_per_epoch: int = 0
samples_per_epoch: int = 0
grad_norm: Optional[float] = 1.0
learning_rate: float = 3e-4
max_seq_len: int = 1024
batch_size: int = 64
epochs: int = 16
n_gpus: int = torch.cuda.device_count()
filter_layers: Optional[Set] = None
def get_loss(self, sample, dataset_split: str):
"""
:param dataset_split: train/valid
:param sample: is the sample
:return: the loss, output and the target
"""
data, target = sample
# Forward pass
with monit.section('Forward pass'):
output = self.model(data.to(self.device))
# Move targets to the same device as output
target = target.to(output.device)
# Calculate loss
loss = self.loss_func(output.view(target.numel(), -1), target.view(-1))
return loss, output, target
def train(self):
for epoch in monit.loop(self.epochs):
self.train_epoch()
tracker.new_line()
def sample(self, idx):
pass
def save_checkpoint(self, idx):
pass
def get_iterators(self):
# Iterate through the batches
iterators = [('train', self.train_loader)]
if self.valid_loader is not None:
iterators.append(('valid', self.valid_loader))
if self.samples_per_epoch > 0:
iterators.append((self.sample, [i for i in range(self.samples_per_epoch)]))
if self.checkpoints_per_epoch > 0:
iterators.append((self.save_checkpoint, [i for i in range(self.checkpoints_per_epoch)]))
return iterators
def train_epoch(self):
# Set model for train
self.model.train()
iterators = self.get_iterators()
for split_name, sample in monit.mix(1024, *iterators):
if split_name == 'train':
# Set gradients to zero
self.optimizer.zero_grad()
tracker.add_global_step()
with torch.set_grad_enabled(split_name == 'train'):
if self.is_amp:
# Forward pass
with amp.autocast():
loss, output, target = self.get_loss(sample, split_name)
else:
loss, output, target = self.get_loss(sample, split_name)
# Get predictions
pred = output.argmax(dim=-1)
# Calculate accuracy
accuracy = pred.eq(target).sum().item() / (target != -100).sum()
tracker.add({f'loss.{split_name}': loss, f'acc.{split_name}': accuracy * 100})
if split_name == 'train':
if self.scaler is not None:
# Backward pass
loss = self.scaler.scale(loss)
# tracker.add({'loss.scaled': loss})
with monit.section('Backward pass'):
loss.backward()
# Optimize
with monit.section('Optimize'):
if self.scaler is None:
self.optimizer.step()
else:
self.scaler.unscale_(self.optimizer)
if self.grad_norm is not None:
torch.nn.utils.clip_grad_norm_(get_trainable_params(self.model), self.grad_norm)
self.scaler.step(self.optimizer)
self.scaler.update()
tracker.save()
@option(TrainerConf.optimizer, 'Adam')
def adam_optimizer(c: TrainerConf):
if c.dtype == torch.float32:
return torch.optim.Adam(get_trainable_params(c.model), lr=c.learning_rate)
elif c.dtype == torch.float16:
from labml_nn.optimizers.adam_fp16 import AdamFP16
return AdamFP16(get_trainable_params(c.model), lr=c.learning_rate)
else:
raise NotImplementedError()
@option(TrainerConf.optimizer, 'SGD')
def sgd_optimizer(c: TrainerConf):
return torch.optim.SGD(get_trainable_params(c.model), lr=c.learning_rate)
@option(TrainerConf.scaler, 'Default')
def grad_scaler(c: TrainerConf):
if not c.is_amp:
return None
if c.dtype == torch.float16:
from labml_nn.optimizers.adam_fp16 import GradScalerFP16
return GradScalerFP16()
else:
return GradScaler()
class PipelineParallelTrainerConf(TrainerConf):
is_checkpointing: bool = False
chunks: int
fine_tuner: FineTuner