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
+32
View File
@@ -0,0 +1,32 @@
"""
---
title: GPT-NeoX
summary: >
Simple GPT-NeoX implementation
---
# GPT-NeoX
This is a simple implementation of [Eleuther GPT-NeoX](https://arxiv.org/abs/2204.06745) for inference and fine-tuning.
* [Model definition](model.html)
* [Tokenizer](tokenizer.html)
* [Checkpoint downloading and loading helpers](checkpoint.html)
* [Utilities](utils/index.html)
* [LLM.int8() quantization](utils/llm_int8.html)
### [Samples](samples/__init__.py)
* [Generating text](samples/generate.html)
* [Fine-tuning the biases with pipeline-parallel](samples/finetune.html)
* [Generating text with LLM.int8()](samples/llm_int8.html)
### [Evaluation](evaluation/__init__.py)
* [Evaluating half precision model on a single GPU](evaluation/half_precision.html)
* [Evaluating LLM.int8() model](evaluation/llm_int8.html)
**Official [Eleuther](https://www.eleuther.ai)
GPT-NoeX is source code is available at [eleutherai/gpt-neox](https://github.com/eleutherai/gpt-neox).**
"""
+164
View File
@@ -0,0 +1,164 @@
"""
---
title: GPT-NeoX Checkpoints
summary: >
Code to download checkpoints and helpers to load them.
---
# GPT-NeoX Checkpoints
"""
from pathlib import Path
from typing import Dict, Union, Tuple, Optional
import torch
from torch import nn
from labml import monit, lab, logger
from labml.logger import Text, inspect
from labml.utils.download import download_file
# Parent url
CHECKPOINTS_URL = 'https://mystic.the-eye.eu/public/AI/models/GPT-NeoX-20B/slim_weights/'
_CHECKPOINTS_DOWNLOAD_PATH: Optional[Path] = None
# Download path
def get_checkpoints_download_path():
global _CHECKPOINTS_DOWNLOAD_PATH
if _CHECKPOINTS_DOWNLOAD_PATH is not None:
return _CHECKPOINTS_DOWNLOAD_PATH
_CHECKPOINTS_DOWNLOAD_PATH = lab.get_data_path() / 'neox_fast' / 'slim_weights'
if not _CHECKPOINTS_DOWNLOAD_PATH.exists():
_CHECKPOINTS_DOWNLOAD_PATH = lab.get_data_path() / 'neox' / 'slim_weights'
inspect(neox_checkpoint_path=_CHECKPOINTS_DOWNLOAD_PATH)
return _CHECKPOINTS_DOWNLOAD_PATH
def get_files_to_download(n_layers: int = 44):
"""
### Get files to download
:return: a list of files to be downloaded
"""
layers = (
# Embedding layer
[0] +
# Transformer layers
list(range(2, 2 + n_layers)) +
# Final normalization layer and readout layer
[47, 48]
)
return (
# Vocabulary and configs
['20B_tokenizer.json', 'configs/20B.yml', 'latest'] +
# Layer checkpoints
[f'global_step150000/layer_{i :02d}-model_{p :02d}-model_states.pt' for i in layers for p in range(2)] +
# Empty states (not used)
[f'global_step150000/mp_rank_{i :02d}_model_states.pt' for i in range(8)]
)
def download(n_layers: int = 44):
"""
## Download all checkpoint files
"""
# Get files to download
files = get_files_to_download(n_layers)
# Iterate
for i, f in monit.enum('Download All', files):
# Log
logger.log(['Downloading ', (f'{i + 1 :3d}/{len(files)}', Text.meta), ': ', (f, Text.value)])
# Download
download_file(CHECKPOINTS_URL + f, get_checkpoints_download_path() / f)
def load_checkpoint_files(files: Tuple[str, str]):
"""
### Load a pair of checkpoint files
:param files: pair of files to load
:return: the loaded parameter tensors
"""
checkpoint_path = get_checkpoints_download_path() / 'global_step150000'
with monit.section('Load checkpoint'):
data = [torch.load(checkpoint_path / f) for f in files]
return data
def merge_params_dim_0(param: Union[nn.Parameter, torch.Tensor], key: str, p1: Dict[str, torch.Tensor],
p2: Dict[str, torch.Tensor]):
"""
### Load a parameter by merging the partitions along first dimension
:param param: is the parameter
:param key: is the name of the parameter
:param p1: first partition dictionary
:param p2: second partition dictionary
"""
w1, w2 = p1[key], p2[key]
param.data[:w1.shape[0]] = w1
param.data[w1.shape[0]:] = w2
def merge_params_dim_1(param: Union[nn.Parameter, torch.Tensor], key: str, p1: Dict[str, torch.Tensor],
p2: Dict[str, torch.Tensor]):
"""
### Load a parameter by merging the partitions along second dimension
:param param: is the parameter
:param key: is the name of the parameter
:param p1: first partition dictionary
:param p2: second partition dictionary
"""
w1, w2 = p1[key], p2[key]
param.data[:, :w1.shape[1]] = w1
param.data[:, w1.shape[1]:] = w2
def merge_params_duplicate(param: Union[nn.Parameter, torch.Tensor], key: str, p1: Dict[str, torch.Tensor],
p2: Dict[str, torch.Tensor]):
"""
### Load an un-partitioned parameter
This does a sanity check to make use both partitions are the same
:param param: is the parameter
:param key: is the name of the parameter
:param p1: first partition dictionary
:param p2: second partition dictionary
"""
w1, w2 = p1[key], p2[key]
diff = sum((w1 - w2) ** 2).item()
assert diff < 1e-4, f'The partitions do not match: {key}'
param.data[:] = (w1 + w2) / 2.
def merge_params_sum(param: Union[nn.Parameter, torch.Tensor], key: str, p1: Dict[str, torch.Tensor],
p2: Dict[str, torch.Tensor]):
"""
### Load biases that are partitioned which gets added on reduce
:param param: is the parameter
:param key: is the name of the parameter
:param p1: first partition dictionary
:param p2: second partition dictionary
"""
w1, w2 = p1[key], p2[key]
param.data[:] = w1 + w2
#
if __name__ == '__main__':
download()
+262
View File
@@ -0,0 +1,262 @@
"""
---
title: Evaluation
summary: >
Code to evaluate the model on NLP tasks through lm-evaluation-harness
---
# Evaluation
This is the code to test the model on
[EleutherAI/lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness).
* [Evaluating half precision model on a single GPU](half_precision.html)
"""
import math
from typing import List
import torch
import torch.nn.functional as F
from lm_eval import tasks, evaluator, utils
from lm_eval.base import BaseLM
from tokenizers import Tokenizer
from torch import nn
from tqdm import tqdm
from labml import monit
from labml_nn.neox.tokenizer import get_tokenizer
class EvalHarnessAdapter(BaseLM):
"""
## Evaluation Harness Adapter
This is based on the [adapter from EleutherAI/gpt-neox](https://github.com/EleutherAI/gpt-neox/blob/main/eval_tasks/eval_adapter.py)
"""
def __init__(self, tokenizer: Tokenizer, vocab_size: int, batch_size: int):
"""
:param tokenizer: is the [Huggingface Tokenizer](huggingface/tokenizers)
:param vocab_size: is the size of the vocabulary
(this differs from the tokenizer vocab size since neox adds some extra to make the embedding layer
model parallel.)
:param batch_size: is the batch size
"""
super().__init__()
self.tokenizer = tokenizer
self._eot_token_id = self.tokenizer.token_to_id("<|endoftext|>")
self._vocab_size = vocab_size
self._batch_size = batch_size
@property
def device(self):
raise RuntimeError()
@property
def vocab_size(self):
"""Size of the vocabulary"""
return self._vocab_size
@property
def eot_token_id(self):
"""End-of-text token"""
return self._eot_token_id
@property
def max_length(self):
"""Maximum sequence length"""
return 2048
@property
def max_gen_toks(self):
"""Maximum number of tokens to generate"""
return 128
@property
def batch_size(self):
"""
Batch size
"""
return self._batch_size
def tok_encode(self, string: str):
"""
Encode a given text
"""
return self.tokenizer.encode(string).ids
def tok_decode(self, tokens: List[int]):
"""
Decode text from token ids
"""
return self.tokenizer.decode(tokens)
def _model_call(self, inps: torch.Tensor):
raise NotImplementedError
def _model_generate(self, context, max_length, eos_token_id):
raise RuntimeError()
def greedy_until(self, requests):
raise RuntimeError()
@torch.no_grad()
def _loglikelihood_tokens(self, requests, disable_tqdm=False):
"""
### Get log-likelihoods of the next tokens
:param requests: List of requests containing the context and the expected continuation.
:param disable_tqdm: If True, disable tqdm progress bar.
"""
# For results
res = []
# Reorder the requests in the descending order of the lengths,
# so that sequences with similar lengths are close
def _collate(x):
toks = x[1] + x[2]
return -len(toks), tuple(toks)
reord = utils.Reorderer(requests, _collate)
# Loop through requests with `batch_size` number of requests at a time
for chunk in utils.chunks(tqdm(reord.get_reordered(), disable=disable_tqdm), self.batch_size):
# To store the inputs for the batch
inps = []
# The continuations for the batch
continuations = []
# Lengths of the input sequences
inplens = []
# Padded length for the batch
padded_length = None
# Loop through each request in the chunk and collect them into PyTorch tensors with paddings
for _, context_enc, continuation_enc in chunk:
# Concatenate the context and continuation
inp = context_enc + continuation_enc
# Truncate from left if the size exceeds the `max_length`
inp = inp[-(self.max_length + 1):]
# Remove final token
inp = inp[:-1]
# Create a tensor
inp = torch.tensor(inp, dtype=torch.long)
# Input length
inplen = inp.shape[0]
# Determine the padded length.
# Shorter sequences will get padded.
if padded_length is None:
padded_length = int(math.ceil(inplen / 32)) * 32
# padded_length = padded_length if padded_length is not None else inplen
# Padding
padding = torch.zeros(padded_length - inplen, dtype=torch.long)
# Add padding
inp = torch.cat([inp, padding], dim=0)
inps.append(inp)
continuations.append(continuation_enc)
inplens.append(inplen)
# Get model logits
logits = self._model_call(torch.stack(inps))
# Get log softmaxes
multi_logits = F.log_softmax(logits, dim=-1)
# Loop through the input/output pairs of the batch
for logits, inplen, cont_toks in zip(multi_logits, inplens, continuations):
# Get number of predicted tokens
contlen = len(cont_toks)
# Get logits of those
logits = logits[inplen - contlen: inplen]
# Get the tokens with the highest probabilities
greedy_tokens = logits.argmax(dim=-1)
# Get the target tokens
cont_toks = torch.tensor(cont_toks, dtype=torch.long).to(logits.device)
# Whether there's an exact match
max_equal = (greedy_tokens == cont_toks).all()
# Log-likelihoods of the target tokens
logits = torch.gather(logits, 1, cont_toks[:, None])
# Add the total log-likelihoods and whether there was a match to the results
res.append((float(logits.sum()), bool(max_equal)))
# Re-order and return results
return reord.get_original(res)
@torch.no_grad()
def run_eval(self, name: str, eval_tasks: List[str]):
"""
### Run given evaluations
"""
# Run [EleutherAI/lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) evaluator
results = evaluator.evaluate(lm=self, task_dict=tasks.get_task_dict(eval_tasks))
# Add configs
results["config"] = {
"name": name,
}
#
return results
class NoeXEvalHarnessAdapter(EvalHarnessAdapter):
"""
## Evaluation Harness Adapter
This is based on the [adapter from EleutherAI/gpt-neox](https://github.com/EleutherAI/gpt-neox/blob/main/eval_tasks/eval_adapter.py)
"""
def __init__(self, model: nn.Module, tokenizer: Tokenizer, vocab_size: int, batch_size: int, device: torch.device):
"""
:param model: is model
:param tokenizer: is the [Huggingface Tokenizer](huggingface/tokenizers)
:param vocab_size: is the size of the vocabulary
(this differs from the tokenizer vocab size since neox adds some extra to make the embedding layer
model parallel.)
:param batch_size: is the batch size
:param device: is the device of the model
"""
super().__init__(tokenizer, vocab_size, batch_size)
self.model = model
self._device = device
def _model_call(self, inps: torch.Tensor):
"""
Call the model
"""
return self.model(inps.to(self._device))
def run_eval_harness(model: nn.Module, name: str, eval_tasks: List[str], device: torch.device, batch_size: int = 8):
"""
## Run evaluation harness with a given model
"""
# Load the tokenizer
with monit.section('Load tokenizer'):
tokenizer = get_tokenizer()
# All tasks if nothing is specified
if not eval_tasks:
eval_tasks = [
"anli_r1",
"anli_r2",
"anli_r3",
"hellaswag",
"lambada",
"piqa",
"winogrande",
"wsc",
"mathqa",
]
# Create the adapter
adapter = NoeXEvalHarnessAdapter(model, tokenizer, 50_432, batch_size, device)
# Run
return adapter.run_eval(name, eval_tasks)
@@ -0,0 +1,48 @@
"""
---
title: Evaluate GPT-NeoX using LLM.int8() quantization on test suite
summary: >
Evaluate GPT-NeoX using LLM.int8() quantization on test suite
---
# Evaluate GPT-NeoX using LLM.int8() quantization on test suite
This code evaluate [GPT-NeoX](../index.html) using, on a suite of tasks.
"""
import argparse
import torch
from torch import nn
from labml_nn.neox.evaluation import run_eval_harness
from labml_nn.neox.model import LayerGenerator
def main():
# Argument parser
parser = argparse.ArgumentParser()
parser.add_argument("--flash", action='store_true', help="whether to use Flash Attention")
opt = parser.parse_args()
# Device
device = torch.device('cuda:0')
# Load layers
layers = list(LayerGenerator(is_clone_layers=True,
filter_layers=None,
dtype=torch.float16,
device=device,
is_flash_attention=opt.flash,
).load())
# Create `nn.Sequential` model
model = nn.Sequential(*layers)
# Run [evaluation harness](index.html)
print(run_eval_harness(model, 'half_precision', ['lambada'], device))
#
if __name__ == '__main__':
main()
+54
View File
@@ -0,0 +1,54 @@
"""
---
title: Evaluate GPT-NeoX using LLM.int8() quantization on test suite
summary: >
Evaluate GPT-NeoX using LLM.int8() quantization on test suite
---
# Evaluate GPT-NeoX using LLM.int8() quantization on test suite
This code evaluate [GPT-NeoX](../index.html) using [LLM.int8() quantization](../utils/llm_int8.html),
on a suite of tasks.
"""
import torch
from torch import nn
from labml import monit
from labml_nn.neox.evaluation import run_eval_harness
from labml_nn.neox.model import LayerGenerator
def main():
# Device
device = torch.device('cuda:0')
# Load layers in float16 into CPU. We convert the layers to int8 later, because doing that
# on the fly after loading layers to GPU causes CUDA memory fragmentation
# (about 3GB memory can get lost due to fragmentation).
layer_generator = LayerGenerator(is_clone_layers=True,
dtype=torch.float16,
device=torch.device('cpu'),
)
# Load layers
layers = list(layer_generator.load())
# This reduces CUDA memory fragmentation
for layer in monit.iterate('Convert to int8', layers, is_children_silent=True):
layer_generator.post_load_prepare(layer,
device=device,
is_llm_int8=True,
llm_int8_threshold=6.0,
)
layer.to(device)
# Create `nn.Sequential` model
model = nn.Sequential(*layers)
# Run [evaluation harness](index.html)
print(run_eval_harness(model, 'half_precision', [], device))
#
if __name__ == '__main__':
main()
+717
View File
@@ -0,0 +1,717 @@
"""
---
title: GPT-NeoX Model Definition
summary: >
This is the model definition of GPT-NeoX.
---
# GPT-NeoX Model
Here is the code for layers of GPT-NeoX model and the code to load
20B checkpoint.
The method `load_state` in the layers load the checkpoints of that layer.
The checkpoint loading helpers are on [`checkpoint.py`](checkpoint.html)
"""
import copy
import math
from typing import Dict, Optional, Set, Callable, Any, Generator, Tuple
import torch
from torch import nn
from torch.cuda.amp import autocast
from labml import monit, logger
from labml.logger import Text
from labml_nn.neox import checkpoint
from labml_nn.neox.utils.cache import get_cache
class NeoXModule(nn.Module):
def load_state(self, p1: Dict[str, torch.Tensor], p2: Dict[str, torch.Tensor]):
pass
class Embedding(NeoXModule):
"""
## Embedding layer
This is a standard embeddings layer with code to load the checkpoint.
"""
def __init__(self, n_vocab: int = 50_432, n_hidden: int = 6_144):
"""
:param n_vocab: is the size of the vocabulary
:param n_hidden: is the size of the embeddings
"""
super().__init__()
self.emb = nn.Embedding(n_vocab, n_hidden)
def forward(self, x: torch.Tensor):
"""
:param x: are the token ids of shape `[batch_size, seq_len]`
"""
return self.emb(x)
def load_state(self, p1: Dict[str, torch.Tensor], p2: Dict[str, torch.Tensor]):
"""
Code to load the checkpoint
"""
with monit.section('Load embedding layer'):
checkpoint.merge_params_dim_0(self.emb.weight, 'word_embeddings.weight', p1, p2)
class RoPE(nn.Module):
"""
## Rotary Positional Embeddings
GPT-NeoX uses [rotary positional embeddings (RoPE)](https://arxiv.org/abs/2104.09864).
WE have annotated implementation of RoPE [here](https://nn.labml.ai/transformers/rope/index.html)
with more notes the theory.
"""
def __init__(self, d_rope: int, base: float = 10_000.):
"""
:param d_rope: is the number of features for RoPE embeddings
:param base: is the base for $\theta_i = 10000^{\frac{2(i-1)}{d}}$, which defaults to $10000$
"""
super().__init__()
# To store $\theta_i$ for the features
self.theta = None
# Cache $\cos m\theta_i$ and $\sin m\theta_i$
self.cos_cached = None
self.sin_cached = None
# Base for $\theta_i = 10000^{\frac{2(i-1)}{d}}$
self.base = base
# Number of features for RoPE
self.d_rope = d_rope
@staticmethod
def rotate_half(x: torch.Tensor):
"""
### Rotate the features
$[-x^{(\frac{d}{2} + 1)}, -x^{(\frac{d}{2} + 2)}, ..., -x^{(d)}, x^{(1)}, x^{(2)}, ..., -x^{(\frac{d}{2})}]$
"""
x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2:]
return torch.cat((-x2, x1), dim=-1)
def forward(self, x: torch.Tensor, offset: int = 0):
"""
:param x: has shape `[..., seq, n_heads, d_k]`
:param offset: is the starting position of `x`. This is $\gt 0$ when we have
cached the keys and queries of previous positions
"""
# Get the actual sequence length
seq_len = x.shape[-3] + offset
# Initialize $\theta$
if self.theta is None:
# $\theta_i = 10000^{\frac{2(i-1)}{d}}$
theta = 1.0 / (self.base ** (torch.arange(0, self.d_rope, 2).float() / self.d_rope))
self.theta = theta.to(x.device).to(x.dtype)
# Initialize $\cos m\theta_i$ and $\sin m\theta_i$ cache
if (
self.cos_cached is None or
seq_len > self.cos_cached.shape[1] or
self.cos_cached.device != x.device or
self.cos_cached.dtype != x.dtype
):
# Get position indexes $m$
seq_idx = torch.arange(seq_len, device=x.device).type_as(self.theta)
# $m \theta_i$
idx_theta = torch.einsum("s,d->sd", seq_idx, self.theta)
# Concatenate so that for row $m$ we have
#
# $$[m \theta_0, m \theta_1, ..., m \theta_{\frac{d}{2}}, m \theta_0, m \theta_1, ..., m \theta_{\frac{d}{2}}]$$
idx_theta2 = torch.cat((idx_theta, idx_theta), dim=-1).to(x.device)
# Calculate $\cos m\theta_i$ and $\sin m\theta_i$ in fp32
with autocast(enabled=False):
idx_theta2 = idx_theta2.float()
# Add head dimension
self.cos_cached = idx_theta2.cos()[:, None, :]
self.sin_cached = idx_theta2.sin()[:, None, :]
# Cache them
self.cos_cached = self.cos_cached.to(x.dtype)
self.sin_cached = self.sin_cached.to(x.dtype)
# Split the features. We apply RoPE to only `d_rope` features
x_rope, x_pass = x[..., :self.d_rope], x[..., self.d_rope:]
# Get the sin and cos values from the cache
cos, sin = self.cos_cached[offset: seq_len], self.sin_cached[offset: seq_len]
# RoPE embeddings
#
# \begin{align}
# \begin{pmatrix}
# x^{(i)}_m \cos m \theta_i - x^{(i + \frac{d}{2})}_m \sin m \theta_i \\
# x^{(i + \frac{d}{2})}_m \cos m\theta_i + x^{(i)}_m \sin m \theta_i \\
# \end{pmatrix} \\
# \end{align}
#
# for $i \in {1, 2, ..., \frac{d}{2}}$
x_rope = (x_rope * cos) + (self.rotate_half(x_rope) * sin)
# Concatenate with features that didn't get RoPE embeddings
return torch.cat((x_rope, x_pass), dim=-1)
class AttentionLayer(nn.Module):
"""
## Attention layer
"""
def __init__(self, n_hidden: int = 6_144, n_heads: int = 64, rope_percentage: float = 0.25,
mask_fill: float = -10_000.0, *, is_flash_attention: bool = False):
"""
:param n_hidden: the number of features in embeddings
:param n_heads: the number of attention heads
:param rope_percentage: percentage of features to add RoPE embeddings
:param mask_fill: masking fill value for attention matrix
:param is_flash_attention: specifies whether to use
[FlashAttention](https://github.com/HazyResearch/flash-attention)
"""
super().__init__()
self.n_heads = n_heads
self.mask_fill = mask_fill
# Linear layer for query, key and value
self.qkv_lin = nn.Linear(n_hidden, n_hidden * 3)
# Final linear layer
self.output = nn.Linear(n_hidden, n_hidden)
# Number of features per head
d_k = n_hidden // n_heads
# RoPE embedding module
self.rope = RoPE(int(d_k * rope_percentage))
# Attention scaling factor
self.scale = 1 / math.sqrt(d_k)
# To cache causal mask
self.causal_mask = None
# Attention softmax module
self.softmax = nn.Softmax(dim=-2)
# [FlashAttention](https://github.com/HazyResearch/flash-attention)
if is_flash_attention:
try:
from flash_attn.flash_attention import FlashAttention
self.flash_attention = FlashAttention()
except ImportError:
logger.log('Install flash attention github.com/HazyResearch/flash-attention. '
'Falling back to normal attention', Text.warning)
self.flash_attention = None
else:
self.flash_attention = None
def _get_mask(self, attn: torch.Tensor):
"""
#### Calculate the causal mask
* `attn` has shape [batch_size, query_seq_len, key_seq_len, n_heads]
"""
# Query and key lengths
nq, nk = attn.shape[1:3]
# Create mask
if (
self.causal_mask is None or
self.causal_mask.shape[0] != nq or
self.causal_mask.shape[1] != nk or
self.causal_mask.device != attn.device
):
self.causal_mask = torch.triu(attn.new_ones([nq, nk], dtype=torch.bool), 1 + nk - nq)
# Return from cache
return self.causal_mask[None, :, :, None]
def forward(self, x: torch.Tensor):
"""
:param x: has shape `[batch_size, seq_len, n_hidden]`
"""
# Get query, key and value embeddings (all concatenated).
# The last dimension size will change from n_hidden -> `3 x n_hidden`
qkv = self.qkv_lin(x)
# Split into heads by changing the shape to `[batch_size, seq_len, n_heads, 3 * d_k]`
qkv = qkv.view(*qkv.shape[:-1], self.n_heads, -1)
# Split into query, key and value each of shape `[batch_size, seq_len, n_heads, 3 * d_k]`
q, k, v = torch.split(qkv, qkv.shape[-1] // 3, dim=-1)
# If we are caching the states of previous tokens
if get_cache().get('use_cache', False):
# Get the state id's. We use to retrieve previous states and store the next states
prev_state_id, next_state_id = get_cache().get('state_ids')
# If there's cache
if prev_state_id is not None:
# Get the past keys and values. These will have shape `[batch_size, prev_seq_len, n_heads, d_k]`
k_past, v_past = get_cache().pop(f'attn_kv_{prev_state_id}')
# Offset of the current embeddings
offset = k_past.shape[1]
# Add RoPE embeddings
q = self.rope(q, offset=offset)
k = self.rope(k, offset=offset)
# Concatenate the past
k = torch.cat([k_past, k], dim=1)
v = torch.cat([v_past, v], dim=1)
else:
# Add RoPE embeddings
q = self.rope(q)
k = self.rope(k)
# Save the current state
get_cache().push(f'attn_kv_{next_state_id}', (k, v))
else:
# No cache - simply add RoPE embeddings
q = self.rope(q)
k = self.rope(k)
# Use flash attention
if self.flash_attention is not None and q.shape[1] == k.shape[1] and q.shape[-1] <= 128:
output = self.compute_flash_attention(q, k, v)
# Otherwise, use normal attention
else:
output = self.compute_attention(q, k, v)
# Reshape from `[batch_size, seq_len, n_heads, d_k] to `[batch_size, seq_len, n_hidden]`
output = output.reshape(*x.shape)
# Final linear layer
return self.output(output)
def compute_flash_attention(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor):
# Stack them into shape `[batch_size, seq_len, 3, n_heads, d_k]`
qkv = torch.stack((q, k, v), dim=2)
d_k = qkv.shape[-1]
if d_k <= 32:
pad = 32 - d_k
elif d_k <= 64:
pad = 64 - d_k
elif d_k <= 128:
pad = 128 - d_k
else:
raise ValueError(f'Head size {d_k} too large for flash attention')
if pad > 0:
qkv = torch.cat((qkv, qkv.new_zeros(*qkv.shape[:-1], pad)), dim=-1)
output, _ = self.flash_attention(qkv, causal=True)
# The output is of shape `[batch_size, seq_len, n_heads, d_k + padding]`
output = output[:, :, :, :d_k]
return output
def compute_attention(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor):
# Disable auto-casting to fp16 for attention computation
with autocast(enabled=False):
if q.dtype == torch.float16:
# Convert to fp32 if the current dtype is fp16
attn = torch.einsum('bihk,bjhk->bijh', q.float(), k.float())
else:
# Do not cast for bfloat
attn = torch.einsum('bihk,bjhk->bijh', q, k)
# Scale attention
attn = attn * self.scale
# Get causal mask
mask = self._get_mask(attn)
# Apply mask
attn.masked_fill_(mask, self.mask_fill)
# Attention softmax
attn = self.softmax(attn)
# Get attention weighted values
output = torch.einsum('bijh,bjhk->bihk', attn.to(v.dtype), v)
return output
class FFNLayer(nn.Module):
"""
## Feedforward Network
"""
def __init__(self, n_hidden: int = 6_144, d_ff: int = 0):
"""
:param n_hidden: is the embedding size
"""
super().__init__()
if not d_ff:
d_ff = n_hidden * 4
# Expansion linear layer
self.dense_h_h4 = nn.Linear(n_hidden, d_ff)
# GELU activation
self.activation = nn.GELU()
# Contraction linear layer
self.dense_h4_h = nn.Linear(d_ff, n_hidden)
def forward(self, x: torch.Tensor):
"""
:param x: has shape `[batch_size, seq_len, n_hidden]`
"""
x = self.dense_h_h4(x)
x = self.activation(x)
x = self.dense_h4_h(x)
return x
class TransformerLayer(NeoXModule):
"""
## Transformer Layer
"""
def __init__(self, n_hidden: int = 6_144, n_heads: int = 64, *, is_flash_attention: bool = False):
"""
:param n_hidden: is the embedding size
:param n_heads: is the number of heads
:param is_flash_attention: specifies whether to use
[FlashAttention](https://github.com/HazyResearch/flash-attention)
*Out implementation doesn't include dropout*.
"""
super().__init__()
# Layer normalization before attention
self.pre_ln_attn = nn.LayerNorm(n_hidden)
# Layer normalization before FFN
self.pre_ln_ffn = nn.LayerNorm(n_hidden)
# Attention layer
self.attention = AttentionLayer(n_hidden, n_heads, is_flash_attention=is_flash_attention)
# FFN layer
self.ffn = FFNLayer(n_hidden)
def forward(self, x: torch.Tensor):
"""
:param x: are the embeddings of shape `[batch_size, seq_len, n_hidden]`
"""
# Residual connection
residual = x
# NeoX runs attention and feedforward network in parallel
attn = self.attention(self.pre_ln_attn(x))
ffn = self.ffn(self.pre_ln_ffn(x))
# Add them and the residual connection
return attn + ffn + residual
def load_state(self, p1: Dict[str, torch.Tensor], p2: Dict[str, torch.Tensor]):
"""
Code to load the checkpoint
"""
with monit.section('Load transformer layer'):
# Attention output transform
checkpoint.merge_params_sum(self.attention.output.bias, 'attention.dense.bias', p1, p2)
checkpoint.merge_params_dim_1(self.attention.output.weight, 'attention.dense.weight', p1, p2)
# Attention query, key and value transform
checkpoint.merge_params_dim_0(self.attention.qkv_lin.bias, 'attention.query_key_value.bias', p1, p2)
checkpoint.merge_params_dim_0(self.attention.qkv_lin.weight, 'attention.query_key_value.weight', p1, p2)
# Layer norm before attention
checkpoint.merge_params_duplicate(self.pre_ln_attn.bias, 'input_layernorm.bias', p1, p2)
checkpoint.merge_params_duplicate(self.pre_ln_attn.weight, 'input_layernorm.weight', p1, p2)
# FFN second transform
checkpoint.merge_params_dim_0(self.ffn.dense_h_h4.bias, 'mlp.dense_h_to_4h.bias', p1, p2)
checkpoint.merge_params_dim_0(self.ffn.dense_h_h4.weight, 'mlp.dense_h_to_4h.weight', p1, p2)
# FFN first transform
checkpoint.merge_params_sum(self.ffn.dense_h4_h.bias, 'mlp.dense_4h_to_h.bias', p1, p2)
checkpoint.merge_params_dim_1(self.ffn.dense_h4_h.weight, 'mlp.dense_4h_to_h.weight', p1, p2)
# Layer norm before FFN
checkpoint.merge_params_duplicate(self.pre_ln_ffn.bias, 'post_attention_layernorm.bias', p1, p2)
checkpoint.merge_params_duplicate(self.pre_ln_ffn.weight, 'post_attention_layernorm.weight', p1, p2)
class FinalNorm(NeoXModule):
"""
## Final normalization layer
"""
def __init__(self, n_hidden: int = 6_144):
"""
:param n_hidden: is the embedding size
"""
super().__init__()
self.ln = nn.LayerNorm(n_hidden)
def forward(self, x: torch.Tensor):
"""
:param x: are the embeddings of shape `[batch_size, seq_len, n_hidden]`
"""
return self.ln(x)
def load_state(self, p1: Dict[str, torch.Tensor], p2: Dict[str, torch.Tensor]):
"""
Code to load the checkpoint
"""
with monit.section('Load final normalization layer'):
checkpoint.merge_params_duplicate(self.ln.bias, 'norm.bias', p1, p2)
checkpoint.merge_params_duplicate(self.ln.weight, 'norm.weight', p1, p2)
class ReadoutLayer(NeoXModule):
"""
Readout layer
"""
def __init__(self, n_hidden: int = 6_144, n_vocab: int = 50_432):
"""
:param n_hidden: is the embedding size
:param n_vocab: is the size of the vocabulary
"""
super().__init__()
self.linear = nn.Linear(n_hidden, n_vocab, bias=False)
def forward(self, x: torch.Tensor):
"""
:param x: are the embeddings of shape `[batch_size, seq_len, n_hidden]`
"""
return self.linear(x)
def load_state(self, p1: Dict[str, torch.Tensor], p2: Dict[str, torch.Tensor]):
"""
Code to load the checkpoint
"""
with monit.section('Load final linear layer'):
checkpoint.merge_params_dim_0(self.linear.weight, 'final_linear.weight', p1, p2)
class LayerGenerator:
pre_created_layers: Dict[Any, Optional[NeoXModule]]
def __init__(self, *, n_vocab: int = 50_432, n_hidden: int = 6_144,
n_layers: int = 44, n_heads: int = 64,
filter_layers: Optional[Set] = None,
is_clone_layers: bool = True,
dtype: torch.dtype = torch.float,
device: torch.device = torch.device('cpu'),
is_llm_int8: bool = False,
llm_int8_threshold: float = 6.0,
is_flash_attention: bool = False
):
"""
### Generator to create layers
The layers are generated in the same order as checkpoints.
It gives `None` when a layer is not available; we use the layer indices as NeoX and there are two
transformation layers we don't need in our implementation.
:param n_vocab: is the number of tokens in the vocabulary
:param n_hidden: is the number of features in the embeddings
:param n_layers: is the number of transformer layers
:param n_heads: is the number of attention heads
:param filter_layers: are the set of layers to be used. All layers will be used if None.
This is used to test smaller versions of the model with fewer layers
:param is_clone_layers: specifies whether to clone the transformer layers (a bit faster)
:param dtype: is the data type of the model
:param device: is the device of the model
:param is_llm_int8: specifies whether to use int8 quantization
:param llm_int8_threshold: is the threshold $\alpha$ used to separate outlier features
:param is_flash_attention: specifies whether to use
[FlashAttention](https://github.com/HazyResearch/flash-attention)
"""
if filter_layers is None:
filter_layers = set(range(n_layers + 3))
self.n_vocab = n_vocab
self.n_hidden = n_hidden
self.n_layers = n_layers
self.n_heads = n_heads
self.filter_layers = filter_layers
self.is_clone_layers = is_clone_layers
self.dtype = dtype
self.device = device
self.is_llm_int8 = is_llm_int8
self.llm_int8_threshold = llm_int8_threshold
self.is_flash_attention = is_flash_attention
self.pre_created_layers = dict(
transformer_layer=None,
)
def _prepare_layer(self, layer: NeoXModule):
"""
#### Prepares the layer for usage
We move the layer to the device and convert it to the correct data type
:param layer: is the layer to prepare
:return: the prepared layer
"""
return layer.to(self.device, self.dtype)
@torch.no_grad()
def post_load_prepare(self, layer: NeoXModule, *,
is_llm_int8: bool = None,
device: torch.device = None,
llm_int8_threshold: float = None,
):
"""
<a id="post_load_prepare"></a>
### Layer transformations after loading the checkpoint
This function implements layer transformations after loading the checkpoint.
Currently, it only applies the int8 quantization.
:param layer: is the layer to prepare
:param is_llm_int8: specifies whether to use int8 quantization
:param device: is the device of the model
:param llm_int8_threshold: is the threshold $\alpha$ used to separate outlier features
:return: the prepared layer
"""
# Get default values if not specified
if is_llm_int8 is None:
is_llm_int8 = self.is_llm_int8
if device is None:
device = self.device
if llm_int8_threshold is None:
llm_int8_threshold = self.llm_int8_threshold
# Skip if not using int8 quantization
if not is_llm_int8:
return layer
# Only convert the linear layers in the transformer layers
if not isinstance(layer, TransformerLayer):
return layer
# Use `make_llm_int8_linear` defined in [utilities](./utils/llm_int8.html).
from labml_nn.neox.utils.llm_int8 import make_llm_int8_linear
# Convert the linear layers
with monit.section('Convert to int8'):
layer.attention.output = make_llm_int8_linear(layer.attention.output,
device=device,
threshold=llm_int8_threshold)
layer.attention.qkv_lin = make_llm_int8_linear(layer.attention.qkv_lin,
device=device,
threshold=llm_int8_threshold)
layer.ffn.dense_h_h4 = make_llm_int8_linear(layer.ffn.dense_h_h4,
device=device,
threshold=llm_int8_threshold)
layer.ffn.dense_h4_h = make_llm_int8_linear(layer.ffn.dense_h4_h,
device=device,
threshold=llm_int8_threshold)
#
return layer
def _create_and_cache_layer(self, name: str, creator: Callable[[], NeoXModule]):
"""
#### Creates and caches a layer
Copying cached layers is faster than initializing new layers because it takes time to
initialize parameters.
:param name: is the name of the layer
:param creator: is the function to create the layer
:return: the created layer or a copy of the cached layer
"""
if not self.is_clone_layers:
return self._prepare_layer(creator())
if self.pre_created_layers[name] is None:
self.pre_created_layers[name] = self._prepare_layer(creator())
layer = copy.deepcopy(self.pre_created_layers[name])
return layer
def _create_transformer_layer(self):
return self._create_and_cache_layer(
'transformer_layer',
lambda: TransformerLayer(self.n_hidden, self.n_heads, is_flash_attention=self.is_flash_attention)
)
def _create_embedding_layer(self):
return Embedding(self.n_vocab, self.n_hidden)
def _create_final_norm_layer(self):
return FinalNorm(self.n_hidden)
def _create_readout_layer(self):
return ReadoutLayer(self.n_hidden, self.n_vocab)
@torch.no_grad()
def get_layers(self) -> Generator[Tuple[NeoXModule, Tuple[str, str]], None, None]:
"""
### Generator to get layers
"""
# Embedding layer
if 0 in self.filter_layers:
with monit.section('Embedding layer'):
layer = self._prepare_layer(self._create_embedding_layer())
yield layer, ('layer_00-model_00-model_states.pt', 'layer_00-model_01-model_states.pt')
# Transformer layers
for i in range(self.n_layers):
# Transformer layer
if i + 1 in self.filter_layers:
with monit.section(f'Transformer Layer {i}'):
yield self._create_transformer_layer(), \
(f'layer_{i + 2 :02d}-model_00-model_states.pt',
f'layer_{i + 2 :02d}-model_01-model_states.pt')
# Final normalization layer
if self.n_layers + 1 in self.filter_layers:
with monit.section('Final norm layer'):
layer = self._prepare_layer(self._create_final_norm_layer())
yield layer, ('layer_47-model_00-model_states.pt', 'layer_47-model_01-model_states.pt')
# Readout layer
if self.n_layers + 2 in self.filter_layers:
with monit.section('Readout layer'):
layer = self._prepare_layer(self._create_readout_layer())
yield layer, ('layer_48-model_00-model_states.pt', 'layer_48-model_01-model_states.pt')
for k in self.pre_created_layers.keys():
self.pre_created_layers[k] = None
@property
def total_layers(self):
"""
### Returns the total number of layers
"""
return self.n_layers + 3
@torch.no_grad()
def load(self) -> Generator[NeoXModule, None, None]:
"""
### Generator to load layers
"""
with monit.section("Layers"):
for i, (layer, files) in enumerate(self.get_layers()):
if files is not None:
layer.load_state(*checkpoint.load_checkpoint_files(files))
layer = self.post_load_prepare(layer)
monit.progress(min(0.99, (i + 1) / self.total_layers))
yield layer
+53
View File
@@ -0,0 +1,53 @@
# GPT-NeoX
This is a simple implementation of [Eleuther GPT-NeoX](https://arxiv.org/abs/2204.06745) for inference and fine-tuning.
* [Model definition](https://nn.labml.ai/neox/model.html)
* [Tokenizer](https://nn.labml.ai/neox/tokenizer.html)
* [Checkpoint downloading and loading helpers](https://nn.labml.ai/neox/checkpoint.html)
* [Utilities](https://nn.labml.ai/neox/utils/index.html)
### [Samples](https://nn.labml.ai/neox/samples/__init__.py)
* [Generating text](https://nn.labml.ai/neox/samples/generate.html)
* [Fine tuning the biases with pipeline-parallel](https://nn.labml.ai/neox/samples/finetune.html)
### [Evaluation](https://nn.labml.ai/neox/evaluation/__init__.py)
* [Evaluating half precision model on a single GPU](https://nn.labml.ai/neox/evaluation/half_precision.html)
### Evaluation Results
| Task | Metric | NeoX Impl (2 GPU) | This repo (1 GPU) | LLM.Int8 |
|------------|-----------------|-------------------|-------------------|----------|
| anli_r1 | acc | 0.3270 | 0.3360 | 0.3440 |
| | acc_stderr | 0.0148 | 0.0149 | 0.0150 |
| anli_r2 | acc | 0.3410 | 0.3350 | 0.3540 |
| | acc_stderr | 0.0150 | 0.0149 | 0.0151 |
| anli_r3 | acc | 0.3567 | 0.3525 | 0.3567 |
| | acc_stderr | 0.0138 | 0.0149 | 0.0138 |
| hellaswag | acc | 0.5351 | 0.5353 | 0.5348 |
| | acc_stderr | 0.0050 | 0.0050 | 0.0050 |
| | acc_norm | 0.7140 | 0.7145 | 0.7132 |
| | acc_norm_stderr | 0.0045 | 0.0045 | 0.0045 |
| lambada | acc | 0.7211 | 0.7204 | 0.7155 |
| | acc_stderr | 0.0062 | 0.0063 | 0.0063 |
| | ppl | 3.6760 | 3.6375 | 3.7245 |
| | ppl_stderr | 0.0760 | 0.0747 | 0.0768 |
| piqa | acc | 0.7748 | 0.7758 | 0.7769 |
| | acc_stderr | 0.0097 | 0.0097 | 0.0097 |
| | acc_norm | 0.7786 | 0.7845 | 0.7829 |
| | acc_norm_stderr | 0.0097 | 0.0096 | 0.0096 |
| winogrande | acc | 0.6598 | 0.6582 | 0.6606 |
| | acc_stderr | 0.0133 | 0.0133 | 0.0133 |
| wsc | acc | 0.5096 | 0.5000 | 0.5288 |
| | acc_stderr | 0.0493 | 0.0493 | 0.0492 |
| mathqa | acc | | | 0.2720 |
| | acc_stderr | | | 0.0081 |
| | acc_norm | | | 0.2727 |
| | acc_norm_stderr | | | 0.0082 |
**Official [Eleuther](https://www.eleuther.ai)
GPT-NoeX is source code is available at [eleutherai/gpt-neox](https://github.com/eleutherai/gpt-neox).**
+12
View File
@@ -0,0 +1,12 @@
"""
---
title: Samples
summary: >
Samples for inference and fine-tuning
---
# Samples
* [Generating text](generate.html)
* [Fine tuning the biases with pipeline-parallel training](finetune.html)
"""
+128
View File
@@ -0,0 +1,128 @@
"""
---
title: Fine Tune GPT-NeoX
summary: >
Fine tune GPT-NeoX biases with Fairscale pipeline parallel module
---
# Fine Tune GPT-NeoX
This shows how to fine tune GPT-NeoX with pipeline parallelism.
"""
import fairscale
import torch
import torch.nn as nn
import torch.utils.data
import torch.utils.data
import typing
from torch.utils.data import DataLoader, RandomSampler
from labml import experiment, monit, tracker, lab
from labml.configs import option
from labml.logger import inspect
from labml_nn.neox.utils.text_dataset import get_training_data
from labml_nn.neox.utils.finetune import FineTuneBiases
from labml_nn.neox.model import LayerGenerator, NeoXModule
from labml_nn.neox.utils import balance_layers_simple
from labml_nn.neox.utils.trainer import PipelineParallelTrainerConf
@option(PipelineParallelTrainerConf.layers, 'PipelineBiases')
def neox_layers(c: PipelineParallelTrainerConf):
"""
### Load GPT-NeoX layers
"""
return list(LayerGenerator(is_clone_layers=c.is_clone_layers,
filter_layers=c.filter_layers,
dtype=c.dtype,
).load())
@option(PipelineParallelTrainerConf.fine_tuner, 'PipelineBiases')
def fine_tune_biases(c: PipelineParallelTrainerConf):
"""
### Create fine tuner for biases
"""
fine_tuner = FineTuneBiases(typing.cast(typing.List[NeoXModule], c.layers))
# Mark biases as trainable
fine_tuner.set_trainable_params()
#
return fine_tuner
@option(PipelineParallelTrainerConf.model, 'PipelineBiases')
def pipe_model(c: PipelineParallelTrainerConf):
"""
### Create pipeline parallel model
"""
if c.is_checkpointing:
raise NotImplementedError()
else:
layers = c.layers
# Make sure the finetuner is initialized
_ = c.fine_tuner
# Create the Pipe module
with monit.section('Pipe'):
# Get the layer distribution across GPUs
balance = balance_layers_simple(len(layers), c.n_gpus)
inspect(balance=balance)
# Devices for each GPU
devices = [torch.device(f'cuda:{i}') for i in range(c.n_gpus)]
# Create Fairscale Pipe module
pipe_model = fairscale.nn.Pipe(nn.Sequential(*layers),
balance=balance,
devices=devices,
chunks=c.chunks)
#
return pipe_model
@option(PipelineParallelTrainerConf.train_loader)
def tiny_shakespeare(c: PipelineParallelTrainerConf):
"""
#### Tiny Shakespeare dataset
"""
dataset = get_training_data(c.max_seq_len)
return DataLoader(dataset,
batch_size=c.batch_size,
sampler=RandomSampler(dataset, replacement=True))
def main():
# Create experiment
experiment.create(name='pipe_neox_biases',
writers={'screen', 'web_api'})
# Initialize configs
conf = PipelineParallelTrainerConf()
experiment.configs(conf, {
'learning_rate': 3e-4,
'is_checkpointing': False,
'max_seq_len': 128,
'batch_size': 64,
'chunks': 8,
})
# Start the experiment
with experiment.start():
# Initialize the model. Do this before the loop for cleaner logs.
_ = conf.model
# Train
for epoch in monit.loop(conf.epochs):
conf.train_epoch()
tracker.new_line()
torch.save(conf.fine_tuner.state_dict(), str(lab.get_data_path() / 'fine_tune.pt'))
#
if __name__ == '__main__':
main()
+102
View File
@@ -0,0 +1,102 @@
"""
---
title: Generate Text with GPT-NeoX
summary: >
Generate Text with GPT-NeoX
---
# Generate Text with GPT-NeoX
This shows how to generate text from GPT-NeoX with a single GPU.
This needs a GPU with more than 45GB memory.
"""
# Imports
from typing import List
import torch
from torch import nn
from labml import monit
from labml_nn.neox.model import LayerGenerator
from labml_nn.neox.utils import get_tokens, print_tokens
from labml_nn.neox.utils.cache import get_cache
# List of layers to load. This is used for testing.
# You can assign a subset of layers like `{0, 1}` so that it only loads
# the first to transformer layers.
LAYERS = None
# Prompt to complete
PROMPT = 'Einstein was born in the German Empire, but moved to Switzerland in 1895, forsaking his German'
def infer(model: nn.Module, ids: List[int], device: torch.device):
"""
### Predict the next token
:param model: is the model
:param ids: are the input token ids
:param device: is the device of the model
"""
with torch.no_grad():
# Get the tokens
x = torch.tensor(ids)[None, :].to(device)
# Eval model
x = model(x)
# Return predicted token
return x[0].max(dim=-1)[1].tolist()
def generate():
"""
## Generate text
"""
# Setup [cache](../utils/cache.html) to cache intermediate key/value pairs for faster generation
cache = get_cache()
cache.set('use_cache', True)
# Device
device = torch.device('cuda:0')
# Load layers
layers = list(LayerGenerator(is_clone_layers=True,
filter_layers=LAYERS,
dtype=torch.float16,
device=device,
).load())
model = nn.Sequential(*layers)
# Get token ids
ids = get_tokens(PROMPT)
# Run the model
cache.set('state_ids', (None, 1))
with monit.section('Infer'):
next_token = infer(model, ids, device)[-1]
# Append the predicted token
ids += [next_token]
# Predict 100 tokens
for i in range(1, 100):
# Set the state to use cached activations
cache.set('state_ids', (i, i + 1))
# Get next token. Note that we only feed the last token to the model because
# we cache the key/value pairs of previous tokens.
with monit.section('Infer'):
next_token = infer(model, [next_token], device)[-1]
# Append the predicted token
ids += [next_token]
# Print
print_tokens(ids, [ids])
#
if __name__ == '__main__':
generate()
+91
View File
@@ -0,0 +1,91 @@
"""
---
title: Generate Text with GPT-NeoX using LLM.int8() quantization
summary: >
Generate Text with GPT-NeoX using LLM.int8() quantization
---
# Generate Text with GPT-NeoX using LLM.int8() quantization
This shows how to generate text from GPT-NeoX using [LLM.int8() quantization](../utils/llm_int8.html).
This needs a GPU with 24GB memory.
"""
import torch
from torch import nn
from labml import monit
from labml_nn.neox.model import LayerGenerator
from labml_nn.neox.samples.generate import PROMPT, infer
from labml_nn.neox.utils import get_tokens, print_tokens
from labml_nn.neox.utils.cache import get_cache
def generate():
"""
## Generate text
"""
# Setup [cache](../utils/cache.html) to cache intermediate key/value pairs for faster generation
cache = get_cache()
cache.set('use_cache', True)
# Device
device = torch.device('cuda:0')
# Load layers in float16 into CPU. We convert the layers to int8 later, because doing that
# on the fly after loading layers to GPU causes CUDA memory fragmentation
# (about 3GB memory can get lost due to fragmentation).
layer_generator = LayerGenerator(is_clone_layers=True,
dtype=torch.float16,
device=torch.device('cpu'),
is_llm_int8=False,
)
layers = list(layer_generator.load())
# This reduces CUDA memory fragmentation
for layer in monit.iterate('Convert to int8', layers, is_children_silent=True):
layer_generator.post_load_prepare(layer,
device=device,
is_llm_int8=True,
llm_int8_threshold=6.0,
)
layer.to(device)
# Create `nn.Sequential` model
model = nn.Sequential(*layers)
# Clear cache and print memory summary for debugging
torch.cuda.empty_cache()
print(torch.cuda.memory_summary())
# Get token ids
ids = get_tokens(PROMPT)
# Run the model.
# We use the [`infer`](generate.html) function defined in [`generate.py`](generate.html)
cache.set('state_ids', (None, 1))
with monit.section('Infer'):
next_token = infer(model, ids, device)[-1]
# Append the predicted token
ids += [next_token]
# Predict 100 tokens
for i in range(1, 100):
# Set the state to use cached activations
cache.set('state_ids', (i, i + 1))
# Get next token. Note that we only feed the last token to the model because
# we cache the key/value pairs of previous tokens.
with monit.section('Infer'):
next_token = infer(model, [next_token], device)[-1]
# Append the predicted token
ids += [next_token]
# Print
print_tokens(ids, [ids])
#
if __name__ == '__main__':
generate()
+28
View File
@@ -0,0 +1,28 @@
"""
---
title: GPT-NeoX Tokenizer
summary: >
Loads the GPT-NeoX tokenizer
---
# GPT-NeoX Tokenizer
This initializes a Hugging Face tokenizer from the downloaded vocabulary.
"""
from tokenizers import Tokenizer
from labml import lab, monit
@monit.func('Load NeoX Tokenizer')
def get_tokenizer() -> Tokenizer:
"""
### Load NeoX Tokenizer
:return: the tokenizer
"""
vocab_file = lab.get_data_path() / 'neox' / 'slim_weights' / '20B_tokenizer.json'
tokenizer = Tokenizer.from_file(str(vocab_file))
return tokenizer
+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