summarize_from_feedback/query_response_model.py (530 lines of code) (raw):
import json
import os
import re
import shutil
import uuid
from dataclasses import dataclass, field
from typing import Callable, Optional, Set, List, Dict
import blobfile as bf
import numpy as np
import torch
from mpi4py import MPI
import summarize_from_feedback
from summarize_from_feedback.model_layout import ModelLayout
from summarize_from_feedback.models import sample_fns
from summarize_from_feedback.models.loss_functions import softmax_xent_loss_fn
from summarize_from_feedback.models.transformer import Hyperparams
from summarize_from_feedback.models.transformer import build_with_random_weights
from summarize_from_feedback.utils import exact_div, hyperparams
from summarize_from_feedback.utils import blobs
from summarize_from_feedback.utils.dist_utils import (
setup_cuda_device_and_dist,
create_data_parallel_comm,
create_within_replica_comm,
)
from summarize_from_feedback.utils.nested import map_nested
from summarize_from_feedback.utils.torch_utils import nans, to_numpy
@dataclass
class RunParams(hyperparams.HParams):
fp16_embedding_weights: bool = False
fp16_conv_weights: bool = False
attn_dropout: float = 0.0
resid_dropout: float = 0.0
emb_dropout: float = 0.0
n_shards: int = 1
def all_gpu_layout(self):
return ModelLayout.standard(
n_shards=self.n_shards,
total_gpus=MPI.COMM_WORLD.Get_size(),
my_rank=MPI.COMM_WORLD.Get_rank(),
)
def sample(
self,
contexts,
sample_len,
sample_fn,
act_dtype=torch.float16,
model_output_keys=(),
**model_call_kwargs,
):
assert not self.training
n_batch, n_ctx = contexts.shape
with torch.no_grad():
tokens = []
logprobs = []
extra_outputs = []
output = self(contexts, act_dtype=act_dtype, **model_call_kwargs)
past_hidden_state = output["hidden_state"].detach()
prev_logits = output["logits"][:, -1:, :]
for sample_t in range(n_ctx, n_ctx + sample_len):
new = sample_fn(prev_logits)
new_tokens, new_logits = new.tokens, new.logits
new_logprobs = -softmax_xent_loss_fn(
dict(logits=new_logits.float()), dict(targets=new_tokens), reduction="none"
)
assert new_tokens.shape == (n_batch, 1)
assert new_logprobs.shape == (n_batch, 1)
tokens.append(new_tokens)
logprobs.append(new_logprobs)
extra_outputs.append({k: output[k] for k in model_output_keys})
# NOTE: last iteration is thrown away
output = self(
new_tokens, hidden_state=past_hidden_state, act_dtype=act_dtype, **model_call_kwargs
)
prev_logits = output["logits"]
past_hidden_state = past_hidden_state.concat_with(output["hidden_state"].detach())
tokens = torch.cat(tokens, dim=1)
logprobs = torch.cat(logprobs, dim=1)
extra_outputs = {
k: torch.cat([extra[k] for extra in extra_outputs], dim=1) for k in model_output_keys
}
return dict(tokens=tokens, logprobs=logprobs, **extra_outputs)
class ModelWithHeads(torch.nn.Module):
def __init__(self, model, scalar_heads, d_model, init_scales=1.0):
super().__init__()
self.model = model
self.scalar_head_names = scalar_heads
if not isinstance(init_scales, dict):
init_scales = {head_name: init_scales for head_name in scalar_heads}
self.scalar_heads = torch.nn.ModuleDict()
for name in self.scalar_head_names:
head = torch.nn.Linear(d_model, 1)
init_std = init_scales.get(name, 1.0) / np.sqrt(d_model + 1)
torch.nn.init.normal_(head.weight, std=init_std)
torch.nn.init.zeros_(head.bias)
self.scalar_heads[name] = head
for attr in [
"include_input_embeddings",
"embedding",
"include_pos_embeddings",
"position_embedding",
"include_final_layer_norm",
"include_output_unembeddings",
"ln_f",
"unembedding_weights",
"torso",
"mp_comm",
"n_ctx",
]:
if hasattr(self.model, attr):
setattr(self, attr, getattr(self.model, attr))
def forward(self, *args, **kwargs):
outputs = self.model(*args, **kwargs)
x = outputs["acts"]
for name, head in self.scalar_heads.items():
outputs[name] = torch.squeeze(head(x.type(head.weight.dtype)), dim=-1)
return outputs
def act_shape(self, in_shape):
return self.model.act_shape(in_shape)
@dataclass
class ModelSpec(hyperparams.HParams):
device: str = "cuda"
load_path: str = None
use_cache: bool = True
short_name: Optional[str] = None
init_heads: Optional[List[str]] = None
map_heads: Dict[str, str] = field(default_factory=dict)
run_params: RunParams = field(default_factory=RunParams)
def name(self):
if self.short_name is not None:
return self.short_name
elif self.load_path is not None:
return self.load_path
else:
raise NotImplementedError
def save_exported_model(layout, model, model_H: Hyperparams, save_dir, save_heads: Set[str]):
"""
Exporting a model allows it to be run with a different layout than it was trained with.
Currently, uploading/loading an exported model is slower than saving/restoring a checkpoint,
but if we can get exporting to be sufficiently fast, then we could replace legacy_checkpoints.py with
this "exporting" approach.
"""
if blobs.is_blob_url(save_dir):
local_dir = os.path.join("/tmp", str(uuid.uuid4()))
else:
local_dir = save_dir
os.makedirs(os.path.join(local_dir, "checkpoint"), exist_ok=True)
def export_fine_piece(fine_model_piece_dict: dict, chkpt_prefix: str):
fine_piece_path = os.path.join(
local_dir, "checkpoint", f"{chkpt_prefix}_shard_{layout.shard_idx:03d}.pkl"
)
# print(f"Uploading fine_piece: {fine_piece_path}")
torch.save(fine_model_piece_dict, fine_piece_path)
torch.cuda.synchronize() # Verify that the piece has finished being written
# Export the embeddings
if model.include_input_embeddings:
export_fine_piece(model.embedding.state_dict(), "input_embeddings")
if model.include_pos_embeddings:
export_fine_piece(model.position_embedding.state_dict(), "position_embedding")
# Export the resblocks
for resblock_idx, resblock in enumerate(model.torso.resblocks):
export_fine_piece(resblock.state_dict(), f"resblock_{resblock_idx:04d}")
# Export the final_layer_norm
if model.include_final_layer_norm:
export_fine_piece(model.ln_f.state_dict(), "final_layer_norm")
# Export the unembeddings
if model.include_output_unembeddings:
export_fine_piece({"unembedding_weights": model.unembedding_weights}, "output_unembeddings")
for head in save_heads:
export_fine_piece(model.scalar_heads[head].state_dict(), f"output_head_{head}")
if blobs.is_blob_url(save_dir):
blobs.parallel_copy_recursive(local_dir, save_dir)
shutil.rmtree(local_dir)
def _matches_any_prefix(x, prefixes):
return any([x.startswith(prefix) for prefix in prefixes])
def dim_to_shard(name: str) -> Optional[int]:
if name.startswith("scalar_heads."):
# heads should be the same on all shards
return None
return parameter_name_to_sharding_dim(name)
def parameter_name_to_sharding_dim(name: str) -> Optional[int]:
"""
:returns: None if all parameters are same on all shards, otherwise the dimension to
split upon.
"""
if name in ["embedding.weight", "position_embedding.weight", "unembedding_weights"]:
return -1
if name.startswith("torso.resblocks"):
match = re.search(r"torso\.resblocks\.\d+\.(.*)", name)
torso_part = match.group(1)
if torso_part.startswith("ln_1.") or torso_part.startswith("ln_2."):
return None
if _matches_any_prefix(
torso_part, ["attn.q_proj", "attn.k_proj", "attn.v_proj", "mlp.c_fc"]
):
return -1
if _matches_any_prefix(torso_part, ["attn.c_proj.weight", "mlp.c_proj.weight"]):
return -2
if _matches_any_prefix(torso_part, ["attn.c_proj.bias", "mlp.c_proj.bias"]):
return None
raise RuntimeError(f"Unexpected parameter name: {name}")
if name in ["ln_f.weight", "ln_f.bias"]:
return None
raise RuntimeError(f"Unexpected parameter name: {name}")
def get_shard_fix_factor(name: str, model_H: Hyperparams, old_model_H: Hyperparams) -> float:
# Hack to fix some bugs with our sharding code
if name.startswith("torso.resblocks"):
match = re.search(r"torso\.resblocks\.\d+\.(.*)", name)
torso_part = match.group(1)
# bias is added before all-reduce, which means with more shards, the
# weights are closer to 0 than expected
if _matches_any_prefix(torso_part, ["attn.c_proj.bias", "mlp.c_proj.bias"]):
return float(old_model_H.n_shards) / model_H.n_shards
if (
_matches_any_prefix(
torso_part,
[
"attn.q_proj.weight",
"attn.k_proj.weight",
"attn.q_proj.bias",
"attn.k_proj.bias",
],
)
and old_model_H.use_blocksparse_attn
):
return np.sqrt(np.sqrt(float(old_model_H.n_shards) / old_model_H.heads))
return 1.0
def load_exported_model(
layout: ModelLayout,
model,
model_H: Hyperparams,
load_path: str,
load_heads_map: Dict[str, str],
use_cache: bool = False,
):
"""
:param load_heads_map: maps name in model -> name to load from
"""
if use_cache and blobs.is_blob_url(load_path):
load_path = blobs.download_directory_cached(load_path)
with bf.BlobFile(os.path.join(load_path, "info.json")) as f:
info = json.load(f)
old_model_H = Hyperparams(**info["model_hparams"])
original_n_shards = old_model_H.n_shards
if "n_shards" in info:
assert info["n_shards"] == original_n_shards
assert layout.n_shards == model_H.n_shards
# print("orig n_shards", original_n_shards, "new n_shards", layout.n_shards)
def fetch_single_piece(fine_piece_name):
with bf.BlobFile(os.path.join(load_path, "checkpoint", fine_piece_name), "rb") as f:
return torch.load(f, map_location=torch.device("cpu"))
if original_n_shards % layout.n_shards == 0:
n_chkpt_shards_per_rank = exact_div(original_n_shards, layout.n_shards)
shard_idx_start = n_chkpt_shards_per_rank * layout.shard_idx
load_shard_idxs = range(shard_idx_start, shard_idx_start + n_chkpt_shards_per_rank)
def fetch(chkpt_prefix: str, module_name: str = ""):
sharded_pieces = [
fetch_single_piece(f"{chkpt_prefix}_shard_{shard_idx:03d}.pkl")
for shard_idx in load_shard_idxs
]
model_piece = {}
for k in sharded_pieces[0].keys():
parameter_name = ".".join([module_name, k]) if module_name else k
sharding_dim = dim_to_shard(parameter_name)
if sharding_dim is None:
val = sharded_pieces[0][k]
else:
val = torch.cat([piece[k] for piece in sharded_pieces], dim=sharding_dim)
fix_factor = get_shard_fix_factor(parameter_name, model_H, old_model_H)
model_piece[k] = (val.float() * fix_factor).to(val.dtype)
return model_piece
elif layout.n_shards % original_n_shards == 0:
n_ranks_per_chkpt_shard = exact_div(layout.n_shards, original_n_shards)
shard_idx_to_load = layout.shard_idx // n_ranks_per_chkpt_shard
shard_slice_idx = layout.shard_idx % n_ranks_per_chkpt_shard
def fetch(chkpt_prefix: str, module_name: str = ""):
unsharded_piece = fetch_single_piece(
f"{chkpt_prefix}_shard_{shard_idx_to_load:03d}.pkl"
)
model_piece = {}
for k in unsharded_piece.keys():
parameter_name = ".".join([module_name, k]) if module_name else k
sharding_dim = dim_to_shard(parameter_name)
if sharding_dim is None:
val = unsharded_piece[k]
else:
split_size = exact_div(
unsharded_piece[k].size()[sharding_dim], n_ranks_per_chkpt_shard
)
val = torch.split(
unsharded_piece[k], [split_size] * n_ranks_per_chkpt_shard, dim=sharding_dim
)[shard_slice_idx]
fix_factor = get_shard_fix_factor(parameter_name, model_H, old_model_H)
model_piece[k] = (val.float() * fix_factor).to(val.dtype)
return model_piece
else:
raise NotImplementedError(
f"Tried running a model that was originally created with "
f"{original_n_shards} shards with {layout.n_shards} shards. The new number "
f"of shards must evenly divide or be divisible by the original number of shards."
)
if model.include_input_embeddings:
model.embedding.load_state_dict(fetch("input_embeddings", "embedding"))
if model.include_pos_embeddings:
model.position_embedding.load_state_dict(
fetch("position_embedding", "position_embedding")
)
# fetch the resblocks
for resblock_idx, resblock in enumerate(model.torso.resblocks):
d = fetch(f"resblock_{resblock_idx:04d}", f"torso.resblocks.{resblock_idx}")
if not model_H.get("key_bias"):
d = {k: v for (k, v) in d.items() if "attn.k_proj.bias" not in k}
resblock.load_state_dict(d)
# fetch the final_layer_norm
if model.include_final_layer_norm:
model.ln_f.load_state_dict(fetch("final_layer_norm", "ln_f"))
# fetch the unembeddings
if model.include_output_unembeddings:
# Pull in the one piece
model.load_state_dict(fetch("output_unembeddings"), strict=False)
for model_head, save_head in load_heads_map.items():
model.scalar_heads[model_head].load_state_dict(
fetch(f"output_head_{save_head}", f"scalar_heads.{model_head}")
)
def _split_query_response_output_parts(x, query_length, response_padding_mask):
"""
Given an output x with shape [batch, num_responses, query_length + response_length, *rest],
returns a dictionary with it split into query/response parts with shapes
[batch, query_length + 1, *rest] and [batch, num_responses, response_length + 1, *rest]
"""
assert x.ndim >= 3
rest_shape = x.size()[3:]
d = dict()
# Add this back if it's ever actually useful
# d["query"] = torch.cat(
# [nans([x.size(0), 1, *rest_shape], dtype=x.dtype, device=x.device), x[:, 0, :query_length]],
# dim=1,
# )
if query_length > 0:
d["response"] = x[:, :, query_length - 1 :]
else:
d["response"] = torch.cat(
[
nans([x.size(0), x.size(1), 1, *rest_shape], dtype=x.dtype, device=x.device),
x[:, :, :query_length],
],
dim=2,
)
for _ in range(len(rest_shape)):
response_padding_mask = response_padding_mask.unsqueeze(-1)
# fill with NaNs in places where response had padding
d["response"].masked_fill_(
torch.cat(
[
torch.zeros(
[x.size(0), x.size(1), 1] + [1 for _ in range(len(rest_shape))],
dtype=torch.bool,
device=x.device,
),
response_padding_mask,
],
dim=2,
),
np.nan,
)
return d
PADDING_TOKEN = -1
def _zero_padding_tokens(response_tokens):
mask = response_tokens == PADDING_TOKEN
assert (
not (mask[:, :, 1:] < mask[:, :, :-1]).any().item()
), f"Padding tokens not a suffix {to_numpy(response_tokens)}"
return mask, torch.masked_fill(response_tokens, mask, 0)
def nested_reduce(ds, f):
new_d = {}
for k, v in ds[0].items():
if isinstance(v, dict):
new_d[k] = nested_reduce([d[k] for d in ds], f)
else:
new_d[k] = f([d[k] for d in ds])
return new_d
@dataclass
class SampleHParams(hyperparams.HParams):
temperature: float = 1.0
top_p: float = 1.0
def validate(self, *, prefix=""):
assert (
self.temperature == 1.0 or self.top_p == 1.0
), f"{prefix or 'SampleHParams'}: Cannot set both temperature ({self.temperature}) and top_p ({self.top_p})"
@classmethod
def argmax(cls):
return cls.from_json(dict(top_p=0))
def _get_sample_fn(H: Optional[SampleHParams] = None):
if H is None:
H = SampleHParams()
if H.top_p != 1.0:
assert H.temperature == 1.0
return sample_fns.nucleus_sampler(top_p=H.top_p)
else:
return sample_fns.standard(temperature=H.temperature)
class QueryResponseModel:
"""
Handles sampling, eval, and training with shared queries.
"""
def __init__(
self, spec: ModelSpec, *, layout: ModelLayout, logit_head=True, heads=(), init_scales=1.0
):
device = setup_cuda_device_and_dist(
backend="nccl" if spec.device == "cuda" else "gloo",
master_addr=None,
device=spec.device,
)
self.device = device
self.layout = layout
assert self.layout.n_shards == spec.run_params.n_shards
self.dp_comm = create_data_parallel_comm(layout)
self.in_replica_comm = create_within_replica_comm(layout)
self.logit_head = logit_head
self.heads = heads
self.init_scales = init_scales
self.load(
spec.load_path,
run_params=spec.run_params,
init_heads=spec.init_heads,
map_heads=spec.map_heads,
use_cache=spec.use_cache,
)
if self.device.type == "cuda":
print(
f"Loaded model to {self.device}. CUDA memory allocated: "
f"{torch.cuda.memory_allocated(device=self.device) / 1e9:.2f} GB"
)
def _sync_params(self, params_to_init, heads_to_init):
if self.layout.n_replicas > 1:
for param in params_to_init:
self.dp_comm.broadcast(
param.data,
src=self.layout.dp_sibling_ranks[0],
name="broadcast_params_from_zeroeth_replica",
)
if self.layout.n_shards > 0:
params_to_sync_shards = []
for head in heads_to_init:
params_to_sync_shards.append(self.model.scalar_heads[head].weight)
params_to_sync_shards.append(self.model.scalar_heads[head].bias)
for param in params_to_sync_shards:
self.model.mp_comm.broadcast(
param.data,
src=self.layout.mp_sibling_ranks[0],
name="broadcast_params_from_zeroeth_shard",
)
def _update_model_with_head_info(self, model):
if not self.logit_head:
model.include_output_unembeddings = False
model.unembedding_weights = None
model = ModelWithHeads(
model,
scalar_heads=list(self.heads),
d_model=model.d_model,
init_scales=self.init_scales,
)
model = model.to(self.device)
return model
def load(self, load_path, run_params=None, init_heads=(), map_heads={}, use_cache=False):
"""
Rebuilds everything, but keeps API semantics: model has same layout, and is on the same device, and all heads are the same (although some may be random init)
"""
if use_cache and blobs.is_blob_url(load_path):
load_path = blobs.download_directory_cached(load_path)
with bf.BlobFile(os.path.join(load_path, "info.json")) as f:
info = json.load(f)
self.model_hparams = Hyperparams(info["model_hparams"])
if run_params is not None:
extra_model_H = {k: v for k, v in run_params.to_json().items() if v is not None}
self.model_hparams.update(**extra_model_H)
self.encoder = summarize_from_feedback.encoder
model = build_with_random_weights(
layout=self.layout,
n_vocab=self.encoder.n_vocab,
device=self.device,
model_H=self.model_hparams,
)
self.model = self._update_model_with_head_info(model)
init_heads = set(init_heads or ())
# Load heads from where map_heads says, or the normal head name by default
load_heads_map = {
head: map_heads.get(head, head) for head in self.heads if head not in init_heads
}
load_exported_model(
self.layout,
self.model,
self.model_hparams,
load_path,
load_heads_map=load_heads_map,
use_cache=use_cache,
)
params_to_init = []
for head in init_heads:
params_to_init.append(self.model.scalar_heads[head].weight)
params_to_init.append(self.model.scalar_heads[head].bias)
self._sync_params(params_to_init, heads_to_init=init_heads)
self.barrier("load_finished")
def barrier(self, name=""):
"""
When called on all ranks, waits until all ranks are done
"""
self.in_replica_comm.barrier(name)
self.dp_comm.barrier(name)
def _eval(
self, queries, responses, eval_fn: Callable = None, eval_inputs=None, **model_call_kwargs
):
"""
Run a forward pass. Return all the head values, broadcasted within each replica. If an
eval_fn is passed, return its output across all replicas.
:return: A dict with structure:
eval_stats: structure from eval_fn
[head]: {
# disabled for now: query: [batch, query_len+1]
response: [batch, num_responses, sample_len+1]
}
"""
queries = queries.to(self.device)
responses = responses.to(self.device)
if eval_inputs is not None:
eval_inputs = map_nested(eval_inputs, lambda x: x.to(self.device))
mask, responses = _zero_padding_tokens(responses)
responses_per_query = responses.size(1)
# NOTE: could make this more efficient by sharing context work
tiled_queries = queries.unsqueeze(1).repeat(1, responses_per_query, 1)
run_tokens = torch.cat([tiled_queries, responses], dim=2).flatten(0, 1)
self.model.eval()
with torch.no_grad():
outputs = self.model(run_tokens, **model_call_kwargs)
outputs_mb = dict()
ret = dict()
for k in list(self.heads) + (["logits"] if self.logit_head else []):
reshaped = outputs[k].view(-1, responses_per_query, *outputs[k].size()[1:])
d = _split_query_response_output_parts(reshaped, queries.size(1), mask)
outputs_mb[k] = d
if k in self.heads:
ret[k] = d
if eval_fn is not None:
ret["eval_stats"] = eval_fn(outputs_mb, eval_inputs)
return ret
def _sample(
self,
context_tokens,
sample_len,
partial_responses=None,
responses_per_query=1,
sample_H=None,
**model_call_kwargs,
):
"""
:return: A dict with structure:
samples: [batch, num_responses, sample_len]
logprobs: [batch, num_responses, sample_len]
[head]: {
response: [batch, num_responses, sample_len+1]
}
"""
context_tokens = context_tokens.to(self.device)
self.model.eval()
n_batch, query_length = context_tokens.size()
assert self.logit_head, f"Cannot sample without logit_head"
# NOTE: could do this more efficiently by sharing context work
repeated_context_tokens = context_tokens.unsqueeze(1).repeat(1, responses_per_query, 1)
# Combine query and response so far into new query to be passed to _sample()
if partial_responses is not None:
partial_responses = partial_responses.to(self.device)
repeated_context_tokens = torch.cat((repeated_context_tokens, partial_responses), 2)
sample_fn = _get_sample_fn(sample_H)
flat_context_tokens = repeated_context_tokens.flatten(0, 1)
flat_n_batch, context_len = flat_context_tokens.shape
assert sample_len + context_len <= self.model_hparams["n_ctx"] + 1, (
f"Requested completion {sample_len} is too long for"
f"context {context_len} and model context_len {self.model_hparams.n_ctx}"
)
results = sample(
self.model,
flat_context_tokens,
sample_len=sample_len,
sample_fn=sample_fn,
model_output_keys=self.heads,
**model_call_kwargs,
)
samples = results["tokens"]
logprobs = results["logprobs"]
assert samples.size(-2) == n_batch * responses_per_query
assert logprobs.size(-2) == n_batch * responses_per_query
assert samples.size(-1) == sample_len, f"{samples.size()} vs {sample_len}"
assert logprobs.size(-1) == sample_len, f"{logprobs.size()} vs {sample_len}"
samples = samples.view(n_batch, responses_per_query, sample_len)
logprobs = logprobs.view(n_batch, responses_per_query, sample_len)
output = dict(contexts=context_tokens, samples=samples, logprobs=logprobs)
mask, _ = _zero_padding_tokens(output["samples"])
# NOTE: sample doesn't return eval'ed values on final token
mask = mask[:, :, :-1]
for k in self.heads:
reshaped = results[k].view(n_batch, responses_per_query, *results[k].shape[1:])
output[k] = _split_query_response_output_parts(reshaped, query_length, mask)
return output