File size: 16,673 Bytes
bb23b91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# coding=utf-8
# Copyright 2024 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""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.
    """
    # 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,
    )
    # Optimizer
    optim_fn = optax.adamw(
        lr_scheduler_fn, weight_decay=config.weight_decay, b1=0.9, b2=0.95
    )
    # Clip the gradients to prevent exploding gradients.
    optimizer = optax.chain(optax.clip_by_global_norm(1), optim_fn)

    # Initialize the train state
    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.
    """
    # Decay the learning rate based on our progress.
    progress = (n_tokens - warmup_tokens) / max(
        1, final_tokens - warmup_tokens,
    )
    lr_mult = jnp.where(
        n_tokens < warmup_tokens,
        # Linear warmup.
        n_tokens / jnp.fmax(1, warmup_tokens),
        # Cosine learning rate decay.
        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 = []

    # Initialize the report progress object
    report_progress = periodic_actions.ReportProgress(
        num_train_steps=config.max_steps, writer=writer)

    # Add metrics for profiling if the process index is 0
    if jax.process_index() == 0:
        hooks += [report_progress,
                  periodic_actions.Profile(logdir=workdir, num_profile_steps=5)]
    
    # Initialize the list of training metrics
    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):
        # Get the next batch from the iterator
        batch = next(train_data_iter)
        # Extract the inputs, start index and difficulty level from the batch
        inputs, _, start_index, levels, cand_targets = get_input_start_index(batch, config)
        # Shard across the devices
        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))
        # Explicit reshape (not common_utils.shard) so a zero-width slot dim
        # (K=0 no-latent baseline: cand_targets is (bs, 0, 81)) shards without
        # the ambiguous -1 inference. Identical to shard() when K>0.
        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:])

        # Run the training step
        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)                      # (bs,)
    slot_pos = si3[:, None] + jnp.arange(num_slots)[None, :]   # (bs, K)
    if getattr(config, "cand_slot_mode", "level") == "depth":
        # Every slot the recurrence fills is active, uniformly over the batch.
        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)  # (bs,)
    active_full = jnp.arange(num_slots)[None, :] < k[:, None]  # (bs, K)

    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   # j=0: last clue token; j>0: latent slot j-1
        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)
    # Extract inputs and labels from the batch
    inputs = batch[:, :-1]
    label = batch[:, 1:]

    # Update dropout_rng
    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)

        # The variables label_one_hot and pred_logits both are 3-dimensional tensors with
        # first axis corresponding to batch size, second correspondingn to sequence length 
        # and third corresponding to the row/column/value at a particular cell
        assert label_one_hot.shape == pred_logits.shape, ("one hot label shape",
                                                        label_one_hot.shape,
                                                        label.shape,
                                                        pred_logits.shape)
        
        # Calculate the cross-entropy loss along the last axis
        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[:, :, :]
        )
        # assert ce_loss.ndim == 2, ("ce_loss", ce_loss.shape)

        # Apply masking to the loss: supervise only the solution region,
        # which now starts K latent slots after the clue block.
        mask = np.repeat(
            np.arange(len(ce_loss[0])).reshape(1, -1), len(ce_loss), axis=0
        )
        mask = (mask >= 3 * start_index + num_slots)      

        # Per-token mean (not per-example sum): keeps the LM CE on the same
        # O(1) scale as the per-digit BCE below, so aux_cand_weight~1 actually
        # balances the two instead of the candidate signal being swamped.
        ce_denom = jnp.maximum(mask.sum(), 1.0)
        avg_ce_loss = (ce_loss * mask).sum() / ce_denom

        # ---- Auxiliary multi-candidate BCE loss on the latent slots ----
        # cand_targets: (bs, K, 81) int bitmask (bit d-1 set <=> digit d is a
        # candidate at that slot's stage; slot j already aligned to the puzzle's
        # k=level-2 budget in the data pipeline). Expand to (bs,K,81,9) multi-hot.
        aux_weight = float(getattr(hyperparams, "aux_cand_weight", 1.0))
        if aux_weight == 0.0:
            # Instance arm: the superposition lives in the varying targets, not
            # in a set head. Drop the whole BCE graph so nothing but the LM CE
            # shapes the latents.
            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)  # (bs,K,81,9)
        # Positive-weighted BCE. Candidate masks are sparse (~1-3 of 9 digits
        # "on"), so plain BCE collapses to predicting all-zeros. Up-weighting
        # the positive (candidate-present) term by pos_weight counteracts the
        # imbalance and forces the head to actually predict the candidate set.
        pos_weight = float(getattr(hyperparams, "aux_pos_weight", 5.0))
        log_p = jax.nn.log_sigmoid(cand_logits)                     # log sigmoid(x)
        log_1mp = jax.nn.log_sigmoid(-cand_logits)                  # log(1 - sigmoid(x))
        bce = -(pos_weight * cand_multi_hot * log_p
                + (1.0 - cand_multi_hot) * log_1mp)                 # (bs,K,81,9)
        # Only the empty cells are supervised: clue cells were sentinel-zeroed
        # in the data pipeline, so any cell whose target row is all-zero is a
        # clue and must not contribute. This makes the effective target
        # (#empty cells) x 9 per puzzle rather than 81 x 9.
        cell_mask = (cand_targets > 0).astype(cand_logits.dtype)    # (bs, K, 81)
        # Delta weighting: consecutive stages are near-duplicates (at K=12 only
        # ~14% of cells change per stage, so ~97% of the target bits are copies
        # of the previous slot). Weighting the unchanged cells below 1 stops the
        # objective from being satisfied by echoing slot j-1. Slot 0 has no
        # predecessor, so it is fully weighted.
        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)          # (bs, K)
        bce_per_cell = bce.mean(axis=3)                             # (bs, K, 81), mean over 9 digits
        bce_per_slot = ((bce_per_cell * cell_w).sum(axis=2)
                        / cell_denom)                               # (bs, K), mean over empty cells
        if backtrack:
            # Strict stage-replay: supervise ONLY the stage-t readout = the last
            # active latent slot per example (index k-1), whose candidate target
            # is that stage's grid. Earlier slots and the LM CE are dropped, so
            # this step purely re-derives "apply f exactly t times -> stage-t
            # readout." Injection above still spans all active slots, so the
            # recurrence reaching slot k-1 is intact.
            k_per = active_full.sum(axis=1)                         # (bs,)
            last_idx = (k_per - 1)[:, None]                         # (bs, 1)
            slot_sel = ((jnp.arange(num_slots)[None, :] == last_idx)
                        & active_full)                             # (bs, K)
            slot_active = slot_sel.astype(bce_per_slot.dtype)
        else:
            slot_active = active_full.astype(bce_per_slot.dtype)    # (bs, K)
        aux_denom = jnp.maximum(slot_active.sum(), 1.0)
        avg_aux_loss = (bce_per_slot * slot_active).sum() / aux_denom

        # Backtrack (replay) steps train only from the readout (no LM CE).
        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)

    # Compute the learning rate and perform gradient descent
    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)
    
    # Update training metrics
    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