CoolFace
Modelpublic

emilyxuan/accesscontrol

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
train.py579 linesDownload Raw Back to root
1import os2import warnings3import datetime4import csv5import json6import random7 8from absl import app, flags9import torch10from tensorboardX import SummaryWriter11from torchvision.utils import make_grid, save_image12from tqdm import tqdm13 14from clients import ClientsGroupMultiTargetAttackedNonIID15from model import UNet16from trigger_encoder import TriggerEncoder17 18FLAGS = flags.FLAGS19 20# ===== Basic =====21flags.DEFINE_string('logdir', None, help='log directory')22flags.DEFINE_bool('train', False, help='run training')23flags.DEFINE_string('dataset_name', 'cifar10', help='dataset name: cifar10 / celeba')24flags.DEFINE_string('device', 'auto', help='auto / cpu / cuda / cuda:0 ...')25flags.DEFINE_integer('seed', 42, help='global random seed')26flags.DEFINE_string('resume_ckpt', None, help='path to resume checkpoint')27flags.DEFINE_string('base_logdir', './logs', help='base log directory')28flags.DEFINE_string('run_name', '', help='optional fixed run name; empty means auto timestamp name')29 30# ===== UNet =====31flags.DEFINE_integer('ch', 128, help='base channel of UNet')32flags.DEFINE_multi_integer('ch_mult', [1, 2, 2, 2], help='channel multiplier')33flags.DEFINE_multi_integer('attn', [1], help='add attention to these levels')34flags.DEFINE_integer('num_res_blocks', 2, help='# resblock in each level')35flags.DEFINE_float('dropout', 0.1, help='dropout rate of resblock')36flags.DEFINE_integer('cond_dim', 128, help='condition embedding dimension')37 38# ===== Trigger encoder / conditional training =====39flags.DEFINE_float('neg_ratio', 0.2, help='negative sample ratio inside each batch')40flags.DEFINE_integer('trigger_base_ch', 32, help='base channel of trigger encoder')41flags.DEFINE_integer('unseen_num_points', 2, help='number of active points in unseen trigger')42flags.DEFINE_integer('unseen_grid_size', 8, help='grid size for unseen trigger sampling')43flags.DEFINE_integer('unseen_square_size', 3, help='square size for unseen trigger blocks')44flags.DEFINE_integer('unseen_base_seed', 12345, help='base seed for unseen trigger generation')45flags.DEFINE_integer('blur_kernel_size', 5, help='kernel size for blurred negative images')46flags.DEFINE_integer('blur_repeat', 2, help='how many times to apply avg-pool blur')47 48# ===== Gaussian Diffusion =====49flags.DEFINE_float('beta_1', 1e-4, help='start beta value')50flags.DEFINE_float('beta_T', 0.02, help='end beta value')51flags.DEFINE_integer('T', 1000, help='total diffusion steps')52 53# ===== Training =====54flags.DEFINE_float('lr', 2e-4, help='target learning rate')55flags.DEFINE_integer('img_size', 32, help='image size; overwritten by dataset defaults if needed')56flags.DEFINE_integer('batch_size', 128, help='batch size')57flags.DEFINE_integer('num_workers', 4, help='workers of Dataloader')58flags.DEFINE_bool('parallel', False, help='multi gpu training')59flags.DEFINE_string('split_mode', 'iid', help='iid or noniid')60 61# ===== Fed =====62flags.DEFINE_string('mode', 'access_control', help='training mode: unconditional / conditional_only / access_control')63flags.DEFINE_integer('mid_T', 500, help='mid T split local global')64flags.DEFINE_integer('local_epoch', 1, help='local epoch')65flags.DEFINE_integer('total_round', 300, help='total round')66flags.DEFINE_integer('client_num', 5, help='client num')67flags.DEFINE_integer('save_round', 20, help='save full checkpoint every N rounds; 0 disables')68flags.DEFINE_integer('data_distribution_seed', 42, help='data distribution seed')69flags.DEFINE_float('ema_scale', 0.9999, help='EMA scale')70 71# ===== Sampling / Logging =====72flags.DEFINE_integer('sample_batch_size', 4, help='number of x_T samples for visual sampling')73flags.DEFINE_integer('sample_every', 1, help='save sample images every N rounds; 0 disables')74flags.DEFINE_bool('save_samples', True, help='whether to save sample grids')75flags.DEFINE_bool('save_tensorboard', True, help='whether to write tensorboard images')76flags.DEFINE_bool('save_client_states', True, help='whether to save per-client states inside checkpoint; disable to reduce checkpoint size')77flags.DEFINE_bool('save_global_model', True, help='save raw global model in checkpoint')78flags.DEFINE_bool('save_global_ema_model', True, help='save global EMA model in checkpoint')79flags.DEFINE_integer('sample_start_step', 0, help='sampling start diffusion step')80flags.DEFINE_integer('sample_end_step', 1000, help='sampling end diffusion step')81 82def get_device() -> torch.device:83    if FLAGS.device == 'auto':84        return torch.device('cuda' if torch.cuda.is_available() else 'cpu')85    return torch.device(FLAGS.device)86 87 88def set_seed(seed: int) -> None:89    random.seed(seed)90    torch.manual_seed(seed)91    if torch.cuda.is_available():92        torch.cuda.manual_seed(seed)93        torch.cuda.manual_seed_all(seed)94 95 96def fed_avg_aggregator(net_list, net_freq):97    sum_parameters = None98    sum_ema_parameters = None99 100    for c in range(len(net_list)):101        global_parameters, global_ema_parameters = net_list[c]102        global_parameters = global_parameters.state_dict()103        global_ema_parameters = global_ema_parameters.state_dict()104 105        if sum_parameters is None:106            sum_parameters = {key: var.clone() * net_freq[c] for key, var in global_parameters.items()}107        else:108            for key in sum_parameters:109                sum_parameters[key] = sum_parameters[key] + global_parameters[key] * net_freq[c]110 111        if sum_ema_parameters is None:112            sum_ema_parameters = {key: var.clone() * net_freq[c] for key, var in global_ema_parameters.items()}113        else:114            for key in sum_ema_parameters:115                sum_ema_parameters[key] = sum_ema_parameters[key] + global_ema_parameters[key] * net_freq[c]116 117    return sum_parameters, sum_ema_parameters118 119 120def build_client_triggers(client_num, img_size, num_points=3, grid_size=8, seed=42, square_size=3):121    assert square_size >= 1 and square_size % 2 == 1, 'square_size should be an odd positive integer, e.g. 3'122 123    margin = max(2, img_size // 8)124    xs = torch.linspace(margin, img_size - margin - 1, steps=grid_size).long()125    ys = torch.linspace(margin, img_size - margin - 1, steps=grid_size).long()126    candidate_positions = [(int(y), int(x)) for y in ys for x in xs]127 128    half = square_size // 2129    triggers = []130 131    for client_id in range(client_num):132        trig = torch.full((3, img_size, img_size), -1.0)133        local_rng = random.Random(seed + client_id)134        selected = local_rng.sample(candidate_positions, num_points)135 136        for (y, x) in selected:137            y1 = max(0, y - half)138            y2 = min(img_size, y + half + 1)139            x1 = max(0, x - half)140            x2 = min(img_size, x + half + 1)141            trig[:, y1:y2, x1:x2] = 1.0142 143        triggers.append(trig)144 145    return triggers146 147 148def maybe_load_global_ckpt():149    if FLAGS.resume_ckpt is None:150        return None, 0151 152    ckpt = torch.load(FLAGS.resume_ckpt, map_location='cpu')153    start_round = int(ckpt.get('round', 0))154 155    print(f'Resume from ckpt: {FLAGS.resume_ckpt}')156    print(f'Start round: {start_round + 1}')157    return ckpt, start_round158 159 160def resolve_logdir(dataset_name: str) -> str:161    if FLAGS.run_name:162        run_name = FLAGS.run_name163    else:164        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')165        run_name = f'cond{FLAGS.cond_dim}_neg{FLAGS.neg_ratio}_seed{FLAGS.data_distribution_seed}_{timestamp}'166 167    if dataset_name == 'cifar10':168        return os.path.join(FLAGS.base_logdir, 'cifar10_cond_acsctrl', run_name)169    if dataset_name == 'celeba':170        return os.path.join(FLAGS.base_logdir, 'celeba_cond_acsctrl', run_name)171    raise NotImplementedError(f'Unsupported dataset: {dataset_name}')172 173def make_new_logdir(base_logdir):174    timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')175    return os.path.join(176        base_logdir,177        f'cond{FLAGS.cond_dim}_neg{FLAGS.neg_ratio}_seed{FLAGS.data_distribution_seed}_{timestamp}'178    )179 180 181def load_trigger_encoder_from_ckpt(trigger_encoder, global_ckpt):182    if global_ckpt is None:183        return184    if 'trigger_encoder' in global_ckpt:185        trigger_encoder.load_state_dict(global_ckpt['trigger_encoder'], strict=True)186        print('Loaded trigger_encoder from checkpoint.')187    else:188        print('Warning: no trigger_encoder found in checkpoint. Using fresh init.')189 190 191def resolve_client_triggers(global_ckpt):192    if global_ckpt is not None and 'client_triggers' in global_ckpt:193        client_triggers = [t.clone().cpu() for t in global_ckpt['client_triggers']]194        print('Loaded client_triggers from checkpoint.')195        return client_triggers196 197    print('No client_triggers found in checkpoint, building new client_triggers.')198    return build_client_triggers(199        FLAGS.client_num,200        FLAGS.img_size,201        num_points=2,202        grid_size=8,203        seed=42,204        square_size=3,205    )206 207 208def save_client_trigger_images(client_triggers, client_triggers_dir):209    for i, trig in enumerate(client_triggers):210        trig_vis = (trig + 1) / 2211        save_image(trig_vis, os.path.join(client_triggers_dir, f'client_{i}.png'))212 213    trigger_grid = make_grid([(t + 1) / 2 for t in client_triggers], nrow=min(len(client_triggers), 5))214    save_image(trigger_grid, os.path.join(client_triggers_dir, 'all_clients.png'))215 216 217def dump_resume_info(logdir, global_ckpt):218    resume_info_path = os.path.join(logdir, 'resume_info.json')219    payload = {220        'resume_ckpt': FLAGS.resume_ckpt,221        'resume_round': int(global_ckpt.get('round', 0)) if global_ckpt is not None else 0,222        'resume_has_trigger_encoder': bool(global_ckpt is not None and 'trigger_encoder' in global_ckpt),223        'resume_has_client_triggers': bool(global_ckpt is not None and 'client_triggers' in global_ckpt),224        'resume_has_client_states': bool(global_ckpt is not None and 'client_states' in global_ckpt),225    }226    with open(resume_info_path, 'w', encoding='utf-8') as f:227        json.dump(payload, f, indent=2, ensure_ascii=False)228 229 230def train():231    set_seed(FLAGS.seed)232    device = get_device()233 234    if FLAGS.dataset_name == 'cifar10':235        FLAGS.img_size = 32236    elif FLAGS.dataset_name == 'celeba':237        FLAGS.img_size = 64238    else:239        raise NotImplementedError(f'Unsupported dataset: {FLAGS.dataset_name}')240 241    global_ckpt, start_round = maybe_load_global_ckpt()242    FLAGS.logdir = make_new_logdir(FLAGS.base_logdir)243 244    sample_uncond_dir = os.path.join(FLAGS.logdir, 'sample_uncond') 245    sample_correct_dir = os.path.join(FLAGS.logdir, 'sample_correct')246    sample_none_dir = os.path.join(FLAGS.logdir, 'sample_none')247    sample_unseen_dir = os.path.join(FLAGS.logdir, 'sample_unseen')248    ckpt_dir = os.path.join(FLAGS.logdir, 'checkpoints')249    client_triggers_dir = os.path.join(FLAGS.logdir, 'client_triggers')250    train_metrics_csv = os.path.join(FLAGS.logdir, 'train_metrics.csv')251    sample_metrics_csv = os.path.join(FLAGS.logdir, 'sample_metrics.csv')252    client_data_stats_csv = os.path.join(FLAGS.logdir, 'client_data_stats.csv')253 254    os.makedirs(FLAGS.logdir, exist_ok=True)255    os.makedirs(ckpt_dir, exist_ok=True)256    257    if FLAGS.mode != 'unconditional':258        os.makedirs(client_triggers_dir, exist_ok=True)259        260    if FLAGS.save_samples and FLAGS.sample_every > 0:261        if FLAGS.mode == 'unconditional':262            os.makedirs(sample_uncond_dir, exist_ok=True)263        else:264            os.makedirs(sample_correct_dir, exist_ok=True)265            os.makedirs(sample_none_dir, exist_ok=True)266            os.makedirs(sample_unseen_dir, exist_ok=True)267 268    print('device:', device)269    print('mode:', FLAGS.mode)270    print('dataset:', FLAGS.dataset_name)271    print('cond_dim:', FLAGS.cond_dim)272    print('neg_ratio:', FLAGS.neg_ratio)273    print('logdir:', FLAGS.logdir)274 275    model_cond_dim = None if FLAGS.mode == 'unconditional' else FLAGS.cond_dim276 277    net_model_global = UNet(278        T=FLAGS.T,279        ch=FLAGS.ch,280        ch_mult=FLAGS.ch_mult,281        attn=FLAGS.attn,282        num_res_blocks=FLAGS.num_res_blocks,283        dropout=FLAGS.dropout,284        cond_dim=model_cond_dim,285    ).to(device)286 287    if FLAGS.mode == 'unconditional':288        trigger_encoder = None289        client_triggers = None290    else:291        trigger_encoder = TriggerEncoder(292            in_ch=3,293            base_ch=FLAGS.trigger_base_ch,294            cond_dim=FLAGS.cond_dim,295            img_size=FLAGS.img_size,296            grid_size=8,297            hidden_dim=256,298            use_coord_feat=True,299        ).to(device)300 301        load_trigger_encoder_from_ckpt(trigger_encoder, global_ckpt)302        client_triggers = resolve_client_triggers(global_ckpt)303        save_client_trigger_images(client_triggers, client_triggers_dir)304 305    x_T = torch.randn(FLAGS.sample_batch_size, 3, FLAGS.img_size, FLAGS.img_size, device=device)306 307    writer = SummaryWriter(FLAGS.logdir) if FLAGS.save_tensorboard else None308    if writer is not None:309        writer.flush()310 311    with open(os.path.join(FLAGS.logdir, 'flagfile.txt'), 'w', encoding='utf-8') as f:312        f.write(FLAGS.flags_into_string())313 314    dump_resume_info(FLAGS.logdir, global_ckpt)315 316    with open(train_metrics_csv, 'w', newline='') as f:317        writer_csv = csv.writer(f)318        writer_csv.writerow([319            'round', 'client_id', 'avg_train_loss', 'last_train_loss', 'lr',320            'step_count', 'num_batches', 'num_samples'321        ])322 323    with open(sample_metrics_csv, 'w', newline='') as f:324        writer_csv = csv.writer(f)325        writer_csv.writerow([326            'round', 'client_id', 'trigger_mode', 'sample_mean', 'sample_std',327            'sample_min', 'sample_max'328        ])329 330    clients_group = ClientsGroupMultiTargetAttackedNonIID(331        dataset_name=FLAGS.dataset_name,332        batch_size=FLAGS.batch_size,333        clients_num=FLAGS.client_num,334        device=device,335        mode=FLAGS.mode,336        trigger_encoder=trigger_encoder,337        client_triggers=client_triggers,338        neg_ratio=FLAGS.neg_ratio,339        cond_dim=FLAGS.cond_dim,340        split_mode=FLAGS.split_mode,341        unseen_num_points=FLAGS.unseen_num_points,342        unseen_grid_size=FLAGS.unseen_grid_size,343        unseen_square_size=FLAGS.unseen_square_size,344        unseen_base_seed=FLAGS.unseen_base_seed,345        blur_kernel_size=FLAGS.blur_kernel_size,346        blur_repeat=FLAGS.blur_repeat,347        data_distribution_seed=FLAGS.data_distribution_seed,348        ema_scale=FLAGS.ema_scale,349        num_workers=FLAGS.num_workers,350        beta_1=FLAGS.beta_1,351        beta_T=FLAGS.beta_T,352        T=FLAGS.T,353    )354 355    for i in range(FLAGS.client_num):356        clients_group.clients_set[i].init(357            net_model_global,358            FLAGS.lr,359            FLAGS.parallel,360            global_ckpt=global_ckpt,361        )362 363    client_idx = [i for i in range(FLAGS.client_num)]364    clients_targets = [clients_group.clients_set[c].get_targets_num() for c in client_idx]365    train_data_sum = sum(clients_targets)366 367    with open(client_data_stats_csv, 'w', newline='') as f:368        writer_csv = csv.writer(f)369        writer_csv.writerow(['client_id', 'num_samples'])370        for c in client_idx:371            writer_csv.writerow([c, clients_targets[c]])372 373    for round_idx in range(start_round, FLAGS.total_round):374        tqdm.write(f'\n[Round {round_idx + 1}/{FLAGS.total_round}]')375        net_freq = [clients_targets[c] / train_data_sum for c in client_idx]376 377        net_list = []378        round_train_metrics = []379 380        for c in client_idx:381            model_state, ema_state, train_metrics = clients_group.clients_set[c].local_train(382                round_idx,383                FLAGS.local_epoch,384                mid_T=FLAGS.mid_T,385            )386            net_list.append((model_state, ema_state))387            round_train_metrics.append(train_metrics)388 389        with open(train_metrics_csv, 'a', newline='') as f:390            writer_csv = csv.writer(f)391            for m in round_train_metrics:392                writer_csv.writerow([393                    m['round'], m['client_id'], m['avg_train_loss'], m['last_train_loss'],394                    m['lr'], m['step_count'], m['num_batches'], m['num_samples']395                ])396 397        sum_parameters, sum_ema_parameters = fed_avg_aggregator(net_list, net_freq)398 399        for c in client_idx:400            clients_group.clients_set[c].set_global_parameters(sum_parameters, sum_ema_parameters)401 402        should_sample = FLAGS.save_samples and FLAGS.sample_every > 0 and ((round_idx + 1) % FLAGS.sample_every == 0)403        round_sample_metrics = []404 405        if should_sample:406            if FLAGS.mode == 'unconditional':407                samples_uncond = []408 409                for c in client_idx:410                    client = clients_group.clients_set[c]411                    with torch.no_grad():412                        x_uncond = client.get_sample(413                            x_T,414                            FLAGS.sample_start_step,415                            FLAGS.sample_end_step,416                            trigger_mode='unconditional',417                        )418 419                    samples_uncond.append(x_uncond)420 421                    round_sample_metrics.append({422                        'round': round_idx + 1,423                        'client_id': c,424                        'trigger_mode': 'unconditional',425                        'sample_mean': float(x_uncond.mean().item()),426                        'sample_std': float(x_uncond.std().item()),427                        'sample_min': float(x_uncond.min().item()),428                        'sample_max': float(x_uncond.max().item()),429                    })430 431                samples_uncond = torch.cat(samples_uncond, dim=0)432                grid_uncond = (make_grid(samples_uncond, nrow=x_T.size(0)) + 1) / 2433 434                save_image(grid_uncond, os.path.join(sample_uncond_dir, f'{round_idx + 1}.png'))435 436                if writer is not None:437                    writer.add_image('sample_uncond', grid_uncond, round_idx + 1)438 439            else:440                samples_correct = []441                samples_none = []442                samples_unseen = []443 444                for c in client_idx:445                    client = clients_group.clients_set[c]446                    with torch.no_grad():447                        x_correct = client.get_sample(448                            x_T, FLAGS.sample_start_step, FLAGS.sample_end_step, trigger_mode='correct'449                        )450                        x_none = client.get_sample(451                            x_T, FLAGS.sample_start_step, FLAGS.sample_end_step, trigger_mode='none'452                        )453                        x_unseen = client.get_sample(454                            x_T, FLAGS.sample_start_step, FLAGS.sample_end_step, trigger_mode='unseen', unseen_seed=999455                        )456                    samples_correct.append(x_correct)457                    samples_none.append(x_none)458                    samples_unseen.append(x_unseen)459 460                    for mode_name, sample in [('correct', x_correct), ('none', x_none), ('unseen', x_unseen)]:461                        round_sample_metrics.append({462                            'round': round_idx + 1,463                            'client_id': c,464                            'trigger_mode': mode_name,465                            'sample_mean': float(sample.mean().item()),466                            'sample_std': float(sample.std().item()),467                            'sample_min': float(sample.min().item()),468                            'sample_max': float(sample.max().item()),469                        })470 471                samples_correct = torch.cat(samples_correct, dim=0)472                samples_none = torch.cat(samples_none, dim=0)473                samples_unseen = torch.cat(samples_unseen, dim=0)474 475                grid_correct = (make_grid(samples_correct, nrow=x_T.size(0)) + 1) / 2476                grid_none = (make_grid(samples_none, nrow=x_T.size(0)) + 1) / 2477                grid_unseen = (make_grid(samples_unseen, nrow=x_T.size(0)) + 1) / 2478 479                save_image(grid_correct, os.path.join(sample_correct_dir, f'{round_idx + 1}.png'))480                save_image(grid_none, os.path.join(sample_none_dir, f'{round_idx + 1}.png'))481                save_image(grid_unseen, os.path.join(sample_unseen_dir, f'{round_idx + 1}.png'))482 483                if writer is not None:484                    writer.add_image('sample_correct', grid_correct, round_idx + 1)485                    writer.add_image('sample_none', grid_none, round_idx + 1)486                    writer.add_image('sample_unseen', grid_unseen, round_idx + 1)487 488            with open(sample_metrics_csv, 'a', newline='') as f:489                writer_csv = csv.writer(f)490                for m in round_sample_metrics:491                    writer_csv.writerow([492                        m['round'], m['client_id'], m['trigger_mode'], m['sample_mean'],493                        m['sample_std'], m['sample_min'], m['sample_max']494                    ])495 496        avg_round_loss = sum(m['avg_train_loss'] for m in round_train_metrics) / max(1, len(round_train_metrics))497        print(f'[Round {round_idx + 1}] avg_train_loss={avg_round_loss:.6f}')498 499        if FLAGS.save_round > 0 and (round_idx + 1) % FLAGS.save_round == 0:500            client_states = None501            if FLAGS.save_client_states:502                client_states = {}503                for c in client_idx:504                    client_states[f'client_{c}'] = clients_group.clients_set[c].export_client_state()505 506            max_step_count = max(clients_group.clients_set[c]._step_count for c in client_idx)507 508            global_ckpt_to_save = {509                'round': round_idx + 1,510                'step_count': max_step_count,511                'config': {512                    'mode': FLAGS.mode,513                    'dataset_name': FLAGS.dataset_name,514                    'img_size': FLAGS.img_size,515                    'cond_dim': None if FLAGS.mode == 'unconditional' else FLAGS.cond_dim,516                    'client_num': FLAGS.client_num,517                    'neg_ratio': FLAGS.neg_ratio,518                    'split_mode': FLAGS.split_mode,519                    'data_distribution_seed': FLAGS.data_distribution_seed,520                    'T': FLAGS.T,521                    'ch': FLAGS.ch,522                    'ch_mult': list(FLAGS.ch_mult),523                    'attn': list(FLAGS.attn),524                    'num_res_blocks': FLAGS.num_res_blocks,525                    'dropout': FLAGS.dropout,526                    'lr': FLAGS.lr,527                    'mid_T': FLAGS.mid_T,528                    'local_epoch': FLAGS.local_epoch,529                    'save_round': FLAGS.save_round,530                    'resumed_from': FLAGS.resume_ckpt,531                },532            }533 534            if FLAGS.mode != 'unconditional':535                global_ckpt_to_save['trigger_encoder'] = trigger_encoder.state_dict()536                global_ckpt_to_save['client_triggers'] = [t.cpu() for t in client_triggers]537 538                global_ckpt_to_save['config'].update({539                    'trigger_base_ch': FLAGS.trigger_base_ch,540                    'trigger_img_size': FLAGS.img_size,541                    'trigger_grid_size': 8,542                    'trigger_hidden_dim': 256,543                    'trigger_use_coord_feat': True,544                    'unseen_num_points': FLAGS.unseen_num_points,545                    'unseen_grid_size': FLAGS.unseen_grid_size,546                    'unseen_square_size': FLAGS.unseen_square_size,547                    'unseen_base_seed': FLAGS.unseen_base_seed,548                    'blur_kernel_size': FLAGS.blur_kernel_size,549                    'blur_repeat': FLAGS.blur_repeat,550                })551 552            if FLAGS.save_global_model:553                global_ckpt_to_save['global_model'] = sum_parameters554            if FLAGS.save_global_ema_model:555                global_ckpt_to_save['global_ema_model'] = sum_ema_parameters556            if client_states is not None:557                global_ckpt_to_save['client_states'] = client_states558 559            ckpt_path = os.path.join(ckpt_dir, f'global_ckpt_round{round_idx + 1}.pt')560            torch.save(global_ckpt_to_save, ckpt_path)561            print(f'Saved checkpoint: {ckpt_path}')562 563    if writer is not None:564        writer.close()565 566 567def main(argv):568    del argv569    warnings.simplefilter(action='ignore', category=FutureWarning)570    if FLAGS.train:571        train()572    else:573        print('Use --train to start training.')574        print('Smoke test example: python train_min.py --train --smoke_test --dataset_name=cifar10 --device=cuda')575 576 577if __name__ == '__main__':578    app.run(main)579