File size: 18,372 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
385
386
# 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.

"""This file contains function that coordinates the training and evaluation of the model."""

import functools
import math
import os

from absl import logging
from clu import metric_writers
from flax import jax_utils
from flax import linen as nn
from flax.training import checkpoints
import jax
from jax import random
import jax.numpy as jnp
import numpy as np
import tensorflow as tf
import wandb

from train import data
from train import evaluater
from train import model
from train import trainer



def log_hyperparams_tb(
    config, model_config, initial_variables, tf_summary_writer
):
    """Log hyperparameters to TensorBoard.

    Args:
        config: experiment's ConfigDict
        model_config: model's ConfigDict
        initial_variables: initial hyperparameter values
        tf_summary_writer: SummaryWriter object.

    Returns:
        The SummaryWriter object and the config.
    """
    # Calculate the total number of model parameters
    config.num_model_parameters = sum(
        x.size for x in jax.tree_util.tree_leaves(initial_variables)
    )

    # Convert hyperparameters to tensors
    config_hyperparameters = [
        tf.convert_to_tensor([k, str(v)]) for k, v in config.items()
    ]
    model_config_hyperparameters = [
        tf.convert_to_tensor([k, str(v)])
        for k, v in model_config.__dict__.items()
    ]

    # Log model hyperparameters to TensorBoard
    with tf_summary_writer.as_default():
        tf.summary.text(
            "Model hyperparameters", tf.stack(model_config_hyperparameters), step=0
        )
        tf.summary.text(
            "Config hyperparameters", tf.stack(config_hyperparameters), step=0
        )

    return tf_summary_writer, config



def train_and_evaluate(config, workdir):
    """The training and evaluation loops for the model.

    Args:
        config: experiment's config dictionary.
        workdir: directory to use for logging.
    """
    # Orbax checkpointing requires an absolute path.
    workdir = os.path.abspath(workdir)

    logging.info("Creating training and evaluator dataset iterator")
    curriculum = data.CurriculumState(
        stage=int(getattr(config, "curriculum_start_stage", 1)),
        max_stage=int(getattr(config, "curriculum_max_stage", 6)))
    train_data_iter = data.create_iter(
        config, config.minibatch_size, train=True, curriculum=curriculum)
    eval_data_iter = data.create_iter(config, config.minibatch_size, train=False)

    logging.info("Finished creating training dataset iterator")

    model_config = model.TransformerConfig(
        dtype=config.dtype,
        vocab_size=config.vocab_size,
        seq_len=config.seq_len,
        num_heads=config.num_heads,
        num_layers=config.num_layers,
        emb_dim=config.emb_dim,
        qkv_dim=config.qkv_dim,
        mlp_dim=config.mlp_dim,
        dropout_rate=config.dropout_rate,
        attention_dropout_rate=config.attention_dropout_rate,
        deterministic=False,
        num_latent_slots=int(config.num_latent_slots),
        inject_latents=bool(int(getattr(config, "recurrent_latent", 1))),
    )

    logging.info("train_config: %s", str(model_config.__dict__))
    print(str(model_config.__dict__), flush=True)

    rng = jax.random.PRNGKey(config.seed)
    rng, init_rng, inference_rng = random.split(rng, num=3)

    # Initialize the model and get initial variables. Dummy latent arguments
    # are provided so the latent projector parameters are created at init.
    rng, dropout_rng = jax.random.split(rng)
    input_shape = (config.minibatch_size, config.seq_len)
    net = model.TransformerLMHeadModel(model_config)
    rng_keys = {"params": init_rng, "dropout": dropout_rng}
    K = int(config.num_latent_slots)
    dummy_latents = jnp.zeros(
        (config.minibatch_size, K, config.emb_dim), model_config.dtype)
    dummy_positions = jnp.zeros((config.minibatch_size, K), jnp.int32)
    dummy_active = jnp.zeros((config.minibatch_size, K), bool)
    sample_out, initial_variables = jax.jit(
        net.init_with_output
        )(rng_keys, jnp.ones(input_shape, jnp.int32), dummy_latents,
          dummy_positions, dummy_active)
    
    state, lr_scheduler_fn = trainer.get_state(config, net, initial_variables)
    # Resume-and-extend support: when resuming, start the training loop at the
    # restored optimizer step (not 0) so a larger config.max_steps continues the
    # cosine LR schedule cleanly instead of re-running steps or overshooting.
    start_step = 0
    if config.resume_training:
        state = checkpoints.restore_checkpoint(config.ckpt_loc, state)
        start_step = int(state.step)
        print("----------Restored model from", config.ckpt_loc,
              f"at step {start_step}-----------")

    writer = metric_writers.create_default_writer(
        workdir, asynchronous=False, just_logging=(jax.process_index() > 0))
    tf_summary_writer = tf.summary.create_file_writer(workdir)

    logging.info("config: %s", str(config.__dict__))
    state = jax_utils.replicate(state)

    dropout_rngs = jax.random.split(rng, jax.local_device_count())

    def make_p_train_step(num_passes):
        return jax.pmap(
            functools.partial(
                trainer.train_step,
                config=model_config,
                hyperparams=config,
                learning_rate_fn=lr_scheduler_fn,
                num_passes=num_passes),
            axis_name="batch",
            donate_argnums=(0,))

    # num_passes = latent recurrence depth for this stage, capped by the number
    # of latent slots (K=0 -> 0 passes -> no-latent control baseline).
    # When recurrence is disabled (control), force 0 passes so no hidden state
    # is ever fed back: the slots become static, independent per-stage readouts.
    recurrent = bool(int(getattr(config, "recurrent_latent", 1)))
    passes_per_stage = int(getattr(config, "passes_per_stage", 1))
    def passes_for(stage):
        return min(passes_per_stage * stage, K) if recurrent else 0
    p_train_step = make_p_train_step(passes_for(curriculum.stage))

    p_eval_step = jax.pmap(functools.partial(evaluater.eval_step,
                                             config=model_config.replace(deterministic=True)),
                                             axis_name="batch")
    
    hooks, report_progress, train_metrics = trainer.get_metrics_report_progress(
      config, workdir, writer)

    tf_summary_writer, config = log_hyperparams_tb(
        config, model_config, initial_variables, tf_summary_writer
    )

    promote_threshold = float(getattr(config, "promote_acc_threshold", 0.85))
    promote_loc_threshold = float(getattr(config, "promote_loc_threshold", 0.70))
    promote_patience = int(getattr(config, "promote_patience_steps", 8000))
    min_stage_steps = int(getattr(config, "min_stage_steps", 2000))
    plateau_steps = int(getattr(config, "plateau_steps", 0))
    plateau_delta = float(getattr(config, "plateau_delta", 0.005))
    instance_mode = bool(getattr(config, "instance_dir", None))
    per_stage_inset = {}
    # Best frontier-depth accuracy seen in the current stage, and the step it
    # was last improved: the plateau detector's state.
    stage_best_acc = -1.0
    stage_best_step = start_step
    ckpt_keep = int(getattr(config, "ckpt_keep", 100))
    # Protected directory for per-stage checkpoints (never rolled off).
    stage_ckpt_dir = os.path.join(workdir, "stage_ckpts")
    stage_started_at = start_step

    with metric_writers.ensure_flushes(writer):
        for step in range(start_step, config.max_steps):
            if step%10000 == 0:
                print("Step:", step, flush=True)

            state, metrics = trainer.train_one_step(p_train_step, config, state, 
                                                    step, dropout_rngs, train_data_iter)
            
            for h in hooks:
                h(step)

            if math.isnan(metrics["loss"][0]):
                print("The loss function became nan: This might be due to the choice of hyperparameters.")
                break

            if step % config.eval_every_steps == 0:
                eval_metrics = evaluater.get_eval_metrics(
                    state, eval_data_iter, p_eval_step, config)
                per_level = eval_metrics.pop("per_level_acc")
                per_slot = eval_metrics.pop("per_slot_acc", {})
                per_slot_ch = eval_metrics.pop("per_slot_acc_changed", {})
                per_stage_inset = eval_metrics.pop("per_stage_inset_acc", {})
                per_bin = eval_metrics.pop("per_bin_acc", {})
                def _m(key):
                    v = eval_metrics.get(key, [])
                    return round(float(np.mean(v)), 4) if len(v) else -1.0
                print(step,
                      "stage", curriculum.stage,
                      "loss", round(float(metrics["loss"].mean()), 4),
                      "ce", round(float(metrics["ce_loss"].mean()), 4),
                      "aux_bce", round(float(metrics["aux_loss"].mean()), 4),
                      "| val_acc", _m("acc"),
                      "loc_acc", _m("loc_acc"),
                      "val|loc", _m("val_given_loc_acc"),
                      "| cand_bit_acc", _m("cand_bit_acc"),
                      "cand_set_acc", _m("cand_set_acc"),
                      "cand_set_chg", _m("cand_set_acc_changed"),
                      "| inset", round(
                          float(per_stage_inset.get(curriculum.stage, -1.0)), 4),
                      "promote_need inset>={:.2f} loc>={:.2f}".format(
                          promote_threshold, promote_loc_threshold),
                      "| depth_acc", round(
                          float(per_slot_ch.get(curriculum.stage, -1.0)), 4),
                      flush=True)
                with tf_summary_writer.as_default():
                    tf.summary.scalar("loss", metrics["loss"].mean(), step=step)
                    tf.summary.scalar("ce_loss", metrics["ce_loss"].mean(), step=step)
                    tf.summary.scalar("aux_bce_loss", metrics["aux_loss"].mean(), step=step)
                    tf.summary.scalar(
                        "learning rate", metrics["learning_rate"].mean(), step=step
                        )
                    tf.summary.scalar("curriculum_stage", curriculum.stage, step=step)

                    log_dict = {'loss': metrics["loss"].mean(), 'learning rate': metrics["learning_rate"].mean()}

                    for key in eval_metrics.keys():
                        vals = eval_metrics[key]
                        if not vals:
                            continue
                        tf.summary.scalar(
                            "eval_" + key, np.array(vals).mean(), step=step
                        )
                        log_dict[ "eval_" + key ] = np.array(vals).mean()

                    for lvl, v in per_level.items():
                        if v >= 0:
                            tf.summary.scalar(f"eval_acc_level_{lvl}", v, step=step)
                            log_dict[f"eval_acc_level_{lvl}"] = v

                    for s, v in per_slot.items():
                        if v >= 0:
                            tf.summary.scalar(f"eval_cand_depth_{s}", v, step=step)
                            log_dict[f"eval_cand_depth_{s}"] = v
                    for s, v in per_slot_ch.items():
                        if v >= 0:
                            tf.summary.scalar(f"eval_cand_depth_chg_{s}", v, step=step)

                    if config.use_wandb: wandb.log(log_dict, step=step)

                # ---- Curriculum promotion (ATC-style: threshold + patience) ----
                # Two curricula, one frontier rule. With latent slots the
                # frontier is the DEEPEST active slot, i.e. the newest wave
                # snapshot this stage introduced, scored on changed cells only
                # (unchanged cells are copies of slot j-1 and stay correct for a
                # head that learned nothing new). With the round-count data
                # curriculum the frontier is the newest round-BIN, i.e. the
                # longest-chain puzzles this stage admitted.
                rounds_curric = str(
                    getattr(config, "data_curriculum", "none")) == "rounds"
                has_frontier = int(config.num_latent_slots) > 0 or rounds_curric
                if has_frontier and curriculum.stage < curriculum.max_stage:
                    t = curriculum.stage
                    loc_now = _m("loc_acc")
                    if instance_mode:
                        # Inset = "emitted digit is in the stage-t set", scored
                        # only on location-correct cells. Require BOTH: the
                        # solver-order locations, and values inside the set.
                        # Otherwise a high inset on ~2% loc_acc promotes from
                        # noise. Plateau is also blocked until location is up,
                        # so we cannot skip stage 1 the way the old patience
                        # promotions did at 0.32-0.46.
                        inset_now = per_stage_inset.get(t, -1.0)
                        frontier_acc = float(inset_now) if inset_now >= 0 else -1.0
                    elif int(config.num_latent_slots) > 0:
                        frontier_acc = per_slot_ch.get(t, -1.0)
                        if frontier_acc < 0:
                            frontier_acc = per_slot.get(t, -1.0)
                    else:
                        frontier_acc = per_bin.get(t, -1.0)
                        if frontier_acc < 0:
                            # The frontier bin holds as little as 6.5% of the
                            # corpus, so a single eval can miss it. Fall back to
                            # the pooled accuracy over everything unlocked.
                            seen = [v for b, v in per_bin.items()
                                    if b <= t and v >= 0]
                            frontier_acc = (float(np.mean(seen)) if seen
                                            else -1.0)
                    steps_in_stage = step - stage_started_at
                    # A negative accuracy means "not measured this eval". Never
                    # let that count as evidence: it must not reset the plateau
                    # tracker, and it must not satisfy the threshold or plateau
                    # rule. Only the hard patience cap can fire without a
                    # measurement.
                    measured = frontier_acc >= 0
                    if measured and frontier_acc > stage_best_acc + plateau_delta:
                        stage_best_acc = frontier_acc
                        stage_best_step = step
                    loc_ready = (not instance_mode) or loc_now >= promote_loc_threshold
                    hit_threshold = (measured and loc_ready
                                     and frontier_acc >= promote_threshold)
                    stalled = (measured and loc_ready and plateau_steps > 0
                               and (step - stage_best_step) >= plateau_steps)
                    patience_over = steps_in_stage >= promote_patience
                    if steps_in_stage >= min_stage_steps and (
                            hit_threshold or stalled or patience_over):
                        reason = ("threshold" if hit_threshold
                                  else "plateau" if stalled else "patience")
                        curriculum.stage += 1
                        stage_started_at = step
                        stage_best_acc = -1.0
                        stage_best_step = step
                        p_train_step = make_p_train_step(passes_for(curriculum.stage))
                        what = ("round-bin" if int(config.num_latent_slots) == 0
                                else "depth")
                        print(f"[curriculum] step {step}: promote to stage "
                              f"{curriculum.stage} ({reason}; graduated {what} "
                              f"{t} acc={frontier_acc:.3f} after "
                              f"{steps_in_stage} steps); "
                              f"latent passes={curriculum.stage}, "
                              f"pool/snapshots now 1..{curriculum.stage}",
                              flush=True)
                        if config.save_checkpoint:
                            unrep_state = jax_utils.unreplicate(state)
                            # Rolling checkpoint in the main workdir.
                            checkpoints.save_checkpoint_multiprocess(
                                workdir, unrep_state, step,
                                keep=ckpt_keep, overwrite=True)
                            # Protected copy: the model *entering* this stage,
                            # kept permanently under stage_ckpts/ (never rolled
                            # off), so every stage's checkpoint survives.
                            checkpoints.save_checkpoint_multiprocess(
                                stage_ckpt_dir, unrep_state, step,
                                keep=100, overwrite=True,
                                prefix=f"stage{curriculum.stage}_")

            if config.save_checkpoint and step > 0 and step % config.save_every_steps == 0:
                checkpoints.save_checkpoint_multiprocess(
                    workdir, jax_utils.unreplicate(state), step,
                    keep=ckpt_keep, overwrite=True
                )

        # Final checkpoint at the end of training
        if config.save_checkpoint:
            checkpoints.save_checkpoint_multiprocess(
                workdir, jax_utils.unreplicate(state), config.max_steps,
                keep=ckpt_keep, overwrite=True)