| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """Transformer LM trainer.""" |
|
|
| import functools |
|
|
| from clu import periodic_actions |
| from flax.training import common_utils |
| from flax.training import train_state |
| import jax |
| from jax import numpy as jnp |
| import numpy as np |
| import optax |
| import ml_collections |
|
|
| from train import model |
|
|
|
|
| def get_state(config, net, initial_variables): |
| """Get the train state given an experiment config, a model and initial variables. |
| |
| Args: |
| config: A ConfigDict containing the configuration for the experiment. |
| net: The model to use for training. |
| initial_variables: The initial variables for the model. |
| |
| Returns: |
| A tuple containing the train state and the learning rate schedule. |
| """ |
| |
| lr_scheduler_fn = functools.partial( |
| lr_scheduler, |
| learning_rate=config.learning_rate, |
| warmup_tokens=config.warmup_tokens, |
| final_tokens=config.max_steps, |
| config=config, |
| ) |
| |
| optim_fn = optax.adamw( |
| lr_scheduler_fn, weight_decay=config.weight_decay, b1=0.9, b2=0.95 |
| ) |
| |
| optimizer = optax.chain(optax.clip_by_global_norm(1), optim_fn) |
|
|
| |
| state = train_state.TrainState.create( |
| apply_fn=net.apply, params=initial_variables["params"], |
| tx=optimizer |
| ) |
|
|
| return state, lr_scheduler_fn |
|
|
| def lr_scheduler( |
| n_tokens: int, learning_rate: float, warmup_tokens: int, final_tokens: int, |
| config: ml_collections.ConfigDict, |
| ) -> float: |
| """Learning rate scheduler, adapted from Mikhail Grankin. |
| |
| The learning rate schedule is cosine decay with a warmup period. |
| |
| The learning rate starts at 0 and linearly increases to the given learning |
| rate over the warmup period. After the warmup period, the learning rate |
| decays according to a cosine schedule, with the given learning rate as the |
| maximum value. |
| |
| Args: |
| n_tokens: The number of tokens processed so far. |
| learning_rate: The initial learning rate. |
| warmup_tokens: The number of tokens to warm up over. |
| final_tokens: The total number of tokens to process. |
| config: A ConfigDict containing the configuration for the learning rate |
| schedule. |
| |
| Returns: |
| The learning rate at the given point in the schedule. |
| """ |
| |
| progress = (n_tokens - warmup_tokens) / max( |
| 1, final_tokens - warmup_tokens, |
| ) |
| lr_mult = jnp.where( |
| n_tokens < warmup_tokens, |
| |
| n_tokens / jnp.fmax(1, warmup_tokens), |
| |
| jnp.fmax(config.end_lr_factor, 0.5 * (1.0 + jnp.cos(np.pi * progress))), |
| ) |
| return learning_rate * lr_mult |
|
|
|
|
| def get_metrics_report_progress(config, workdir, writer): |
| """ |
| Get the metrics for reporting progress during training. |
| |
| Args: |
| config: The configuration for the experiment. |
| workdir: The directory for storing the logs. |
| writer: The writer object for recording the metrics. |
| |
| Returns: |
| hooks: List of hooks for tracking progress. |
| report_progress: Object for reporting progress. |
| train_metrics: List of training metrics. |
| """ |
| hooks = [] |
|
|
| |
| report_progress = periodic_actions.ReportProgress( |
| num_train_steps=config.max_steps, writer=writer) |
|
|
| |
| if jax.process_index() == 0: |
| hooks += [report_progress, |
| periodic_actions.Profile(logdir=workdir, num_profile_steps=5)] |
| |
| |
| train_metrics = [] |
| |
| return hooks, report_progress, train_metrics |
|
|
|
|
| def get_input_start_index(batch, config): |
| inputs = jax.tree_util.tree_map(np.asarray, batch[0]) |
| puzzles = jax.tree_util.tree_map(np.asarray, batch[1]) |
| start_index = jax.tree_util.tree_map(np.asarray, batch[2]) |
| levels = jax.tree_util.tree_map(np.asarray, batch[3]) |
| cand_targets = jax.tree_util.tree_map(np.asarray, batch[4]) |
| return inputs, puzzles, start_index, levels, cand_targets |
|
|
|
|
| def train_one_step(p_train_step, config, state, step, dropout_rngs, train_data_iter): |
| """ |
| Single step of the training loop. |
| |
| Args: |
| p_train_step: The training step function. |
| config: The experiment configuration. |
| state: The train state. |
| step: The step number. |
| dropout_rngs: The dropout random number generator. |
| train_data_iter: The iterator for the train data. |
| |
| Returns: |
| The updated train state and train metrics. |
| """ |
| with jax.profiler.StepTraceAnnotation("train", step_num=step): |
| |
| batch = next(train_data_iter) |
| |
| inputs, _, start_index, levels, cand_targets = get_input_start_index(batch, config) |
| |
| inputs = common_utils.shard(jax.tree_util.tree_map(np.asarray, inputs)) |
| start_index = common_utils.shard(jax.tree_util.tree_map(np.asarray, start_index)) |
| levels = common_utils.shard(jax.tree_util.tree_map(np.asarray, levels)) |
| |
| |
| |
| cand_targets = np.asarray(cand_targets) |
| _nd = jax.local_device_count() |
| cand_targets = cand_targets.reshape( |
| (_nd, cand_targets.shape[0] // _nd) + cand_targets.shape[1:]) |
|
|
| |
| state, metrics, _ = p_train_step( |
| state, inputs, start_index, levels, cand_targets, dropout_rng=dropout_rngs |
| ) |
|
|
| return state, metrics |
|
|
|
|
| def build_latent_state(inputs, start_index, levels, config, num_passes, |
| apply_fn, rngs=None): |
| """Build the continuous latent thoughts (Coconut/ATC-style recurrence). |
| |
| Pass 0 seeds z_1 from the last-layer hidden at the last clue token; pass j |
| reads the hidden at latent slot j to produce z_{j+1}. Slot j is active for |
| an example only when j < k, with k = clip(level - 2, 1, K) (difficulty- |
| matched latent budget). Gradients flow through all passes (full BPTT). |
| |
| Returns (latent_vals, slot_pos, active_full). |
| """ |
| num_slots = int(config.num_latent_slots) |
| bs = inputs.shape[0] |
| bidx = jnp.arange(bs) |
|
|
| si3 = 3 * start_index.reshape(-1) |
| slot_pos = si3[:, None] + jnp.arange(num_slots)[None, :] |
| if getattr(config, "cand_slot_mode", "level") == "depth": |
| |
| k = jnp.full((bs,), max(min(num_passes, num_slots), 1), dtype=jnp.int32) |
| else: |
| k = jnp.clip(levels.reshape(-1) - 2, 1, num_slots) |
| active_full = jnp.arange(num_slots)[None, :] < k[:, None] |
|
|
| latent_vals = jnp.zeros((bs, num_slots, config.emb_dim), dtype=config.dtype) |
|
|
| for j in range(num_passes): |
| act_j = active_full & (jnp.arange(num_slots)[None, :] < j) |
| _, hidden, _ = apply_fn(inputs, latent_vals, slot_pos, act_j, rngs) |
| src = si3 - 1 + j |
| z = hidden[bidx, src] |
| latent_vals = latent_vals.at[:, j].set(z.astype(config.dtype)) |
|
|
| return latent_vals, slot_pos, active_full |
|
|
|
|
| def train_step(state, batch, start_index, levels, cand_targets, config, |
| hyperparams, learning_rate_fn, num_passes, dropout_rng=None, |
| backtrack=False): |
| """One step of the training loop. |
| |
| Args: |
| state: Train state. |
| batch: Input batch (bs, 3*81 + K) with latent placeholder slots. |
| start_index: Number of clue cells per example. |
| levels: Puzzle difficulty level (3..8) per example. |
| config: Model config. |
| hyperparams: Hyperparameter dictionary. |
| learning_rate_fn: Learning rate function. |
| num_passes: Number of latent recurrence passes (= curriculum stage). |
| dropout_rng: RNG used for dropout. |
| |
| Returns: |
| A new train state, train metrics, and computed model predictions. |
| """ |
| num_slots = int(config.num_latent_slots) |
| |
| inputs = batch[:, :-1] |
| label = batch[:, 1:] |
|
|
| |
| dropout_rng = jax.random.fold_in(dropout_rng, state.step) |
| dropout_rng_dict = {"dropout": dropout_rng} |
|
|
| def loss_fn(params): |
| """Compute the loss function.""" |
| net = model.TransformerLMHeadModel(config) |
|
|
| def apply_fn(x, lv, lp, la, rngs): |
| return net.apply({"params": params}, x, latent_values=lv, |
| latent_positions=lp, latent_active=la, |
| rngs=rngs) |
|
|
| latent_vals, slot_pos, active_full = build_latent_state( |
| inputs, start_index, levels, config, num_passes, apply_fn, |
| rngs=dropout_rng_dict) |
|
|
| pred_logits, _, cand_logits = apply_fn( |
| inputs, latent_vals, slot_pos, active_full, dropout_rng_dict) |
|
|
| label_one_hot = jax.nn.one_hot(label, num_classes=config.vocab_size) |
|
|
| |
| |
| |
| assert label_one_hot.shape == pred_logits.shape, ("one hot label shape", |
| label_one_hot.shape, |
| label.shape, |
| pred_logits.shape) |
| |
| |
| pred_logits_sol = pred_logits[:, :, :] |
| label_one_hot_sol = label_one_hot[:, :, :] |
|
|
| ce_loss = optax.softmax_cross_entropy( |
| logits=pred_logits_sol[:, :, :], labels=label_one_hot_sol[:, :, :] |
| ) |
| |
|
|
| |
| |
| mask = np.repeat( |
| np.arange(len(ce_loss[0])).reshape(1, -1), len(ce_loss), axis=0 |
| ) |
| mask = (mask >= 3 * start_index + num_slots) |
|
|
| |
| |
| |
| ce_denom = jnp.maximum(mask.sum(), 1.0) |
| avg_ce_loss = (ce_loss * mask).sum() / ce_denom |
|
|
| |
| |
| |
| |
| aux_weight = float(getattr(hyperparams, "aux_cand_weight", 1.0)) |
| if aux_weight == 0.0: |
| |
| |
| |
| zero = jnp.zeros((), dtype=pred_logits.dtype) |
| return avg_ce_loss, (pred_logits, avg_ce_loss, zero) |
|
|
| bits = jnp.arange(9) |
| cand_multi_hot = ((cand_targets[..., None].astype(jnp.int32) |
| >> bits) & 1).astype(cand_logits.dtype) |
| |
| |
| |
| |
| pos_weight = float(getattr(hyperparams, "aux_pos_weight", 5.0)) |
| log_p = jax.nn.log_sigmoid(cand_logits) |
| log_1mp = jax.nn.log_sigmoid(-cand_logits) |
| bce = -(pos_weight * cand_multi_hot * log_p |
| + (1.0 - cand_multi_hot) * log_1mp) |
| |
| |
| |
| |
| cell_mask = (cand_targets > 0).astype(cand_logits.dtype) |
| |
| |
| |
| |
| |
| delta_bg = float(getattr(hyperparams, "aux_delta_bg", 1.0)) |
| if delta_bg != 1.0: |
| changed = jnp.concatenate( |
| [jnp.ones_like(cand_targets[:, :1], dtype=bool), |
| cand_targets[:, 1:] != cand_targets[:, :-1]], axis=1) |
| cell_w = cell_mask * (delta_bg + (1.0 - delta_bg) |
| * changed.astype(cand_logits.dtype)) |
| else: |
| cell_w = cell_mask |
| cell_denom = jnp.maximum(cell_w.sum(axis=2), 1e-6) |
| bce_per_cell = bce.mean(axis=3) |
| bce_per_slot = ((bce_per_cell * cell_w).sum(axis=2) |
| / cell_denom) |
| if backtrack: |
| |
| |
| |
| |
| |
| |
| k_per = active_full.sum(axis=1) |
| last_idx = (k_per - 1)[:, None] |
| slot_sel = ((jnp.arange(num_slots)[None, :] == last_idx) |
| & active_full) |
| slot_active = slot_sel.astype(bce_per_slot.dtype) |
| else: |
| slot_active = active_full.astype(bce_per_slot.dtype) |
| aux_denom = jnp.maximum(slot_active.sum(), 1.0) |
| avg_aux_loss = (bce_per_slot * slot_active).sum() / aux_denom |
|
|
| |
| ce_term = 0.0 if backtrack else avg_ce_loss |
| total_loss = ce_term + aux_weight * avg_aux_loss |
|
|
| return total_loss, (pred_logits, avg_ce_loss, avg_aux_loss) |
|
|
| |
| step = state.step |
| lr = learning_rate_fn(step) |
| (loss, aux), grads = jax.value_and_grad(loss_fn, |
| has_aux=True)(state.params) |
| pred_logits, ce_loss, aux_loss = aux |
| grads = jax.lax.pmean(grads, "batch") |
| new_state = state.apply_gradients(grads=grads) |
| |
| |
| metrics = { |
| "step": step, "loss": loss, "learning_rate": lr, |
| "ce_loss": ce_loss, |
| "aux_loss": aux_loss, |
| "pred_logits": pred_logits, "weights": inputs.shape[0] |
| } |
| |
| return new_state, metrics, pred_logits |
|
|