fela-acml2026 / scripts /chunk_ablation.py
itstheraj's picture
FELA: training code, checkpoints, and evaluation results
a24d5a6
Raw
History Blame
26.2 kB
import argparse
import gc
import json
import logging
import math
import os
import random
import sys
import time
from pathlib import Path
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
_script_dir = os.path.dirname(os.path.abspath(__file__))
_root = os.path.dirname(_script_dir)
if _root not in sys.path:
sys.path.insert(0, _root)
import model_cpu_gpt2 as _m
_m._fla_available = False
from model_cpu_gpt2 import CPUGPT, CPUGPTConfig
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
CHUNK_VARIANTS = [64, 256, 512, 1024]
def make_124m_config(gla_chunk: int) -> CPUGPTConfig:
return CPUGPTConfig(
n_layer=12,
n_embd=768,
n_head=12,
ffn_hidden=2048,
fno_modes=256,
gla_chunk=gla_chunk,
seq_len=8192,
layer_pattern="SSSL",
vocab_size=50257,
)
polar_express_coeffs = [
(8.156554524902461, -22.48329292557795, 15.878769915207462),
(4.042929935166739, -2.808917465908714, 0.5000178451051316),
(3.8916678022926607, -2.772484153217685, 0.5060648178503393),
(3.285753657755655, -2.3681294933425376, 0.46449024233003106),
(2.3465413258596377, -1.7097828382687081, 0.42323551169305323),
]
def adamw_step(p, grad, m, v, step, lr, b1, b2, eps, wd):
p.mul_(1 - lr * wd)
m.lerp_(grad, 1 - b1)
v.lerp_(grad.square(), 1 - b2)
bc1 = 1 - b1**step
bc2 = 1 - b2**step
p.addcdiv_(m / bc1, (v / bc2).sqrt_().add_(eps), value=-lr)
def muon_step(grads_stack, params, mom_buf, lr, momentum=0.95, ns_steps=3):
mom_buf.lerp_(grads_stack, 1 - momentum)
X = mom_buf.float()
X = X / (X.norm(dim=(-2, -1), keepdim=True) * 1.02 + 1e-6)
for a, b, c in polar_express_coeffs[:ns_steps]:
if X.size(-2) >= X.size(-1):
A = X.mT @ X
X = a * X + X @ (b * A + c * (A @ A))
else:
A = X @ X.mT
X = a * X + (b * A + c * (A @ A)) @ X
torch._foreach_sub_(params, list((X * lr).to(params[0].dtype).unbind(0)))
class MuonAdamW(torch.optim.Optimizer):
def __init__(self, param_groups):
super().__init__(param_groups, defaults={})
@torch.no_grad()
def step(self):
for g in self.param_groups:
if g["kind"] == "adamw":
for p in g["params"]:
if p.grad is None:
continue
st = self.state[p]
if not st:
st["step"] = 0
st["m"] = torch.zeros_like(p)
st["v"] = torch.zeros_like(p)
st["step"] += 1
adamw_step(
p,
p.grad,
st["m"],
st["v"],
st["step"],
g["lr"],
*g["betas"],
g["eps"],
g.get("wd", 0),
)
elif g["kind"] == "muon":
params = g["params"]
if not params:
continue
p0 = params[0]
st = self.state[p0]
stacked = torch.stack(
[p.grad for p in params if p.grad is not None]
).float()
if not st:
st["mom"] = torch.zeros_like(stacked)
lr = g["lr"] * max(1.0, p0.shape[-2] / max(p0.shape[-1], 1)) ** 0.5
muon_step(stacked, params, st["mom"], lr, g.get("momentum", 0.95))
def build_optimizer(model, cfg, lr_matrix=0.01, lr_emb=0.02, lr_lm=0.002):
scale = (cfg.n_embd / 768) ** -0.5
raw = (
model.module
if isinstance(model, nn.parallel.DistributedDataParallel)
else model
)
matrix_params, scalar_params = [], []
for block in raw.blocks:
for p in block.parameters():
(matrix_params if p.ndim == 2 else scalar_params).append(p)
groups = [
dict(
kind="adamw",
params=list(raw.wte.parameters()),
lr=lr_emb * scale,
betas=(0.8, 0.95),
eps=1e-8,
),
dict(
kind="adamw",
params=list(raw.lm_head.parameters())
if raw.lm_head.weight is not raw.wte.weight
else [],
lr=lr_lm * scale,
betas=(0.8, 0.95),
eps=1e-8,
),
dict(
kind="adamw",
params=scalar_params,
lr=lr_matrix * scale,
betas=(0.8, 0.95),
eps=1e-8,
),
]
for shape in sorted({p.shape for p in matrix_params}):
ps = [p for p in matrix_params if p.shape == shape]
groups.append(dict(kind="muon", params=ps, lr=lr_matrix, momentum=0.95))
opt = MuonAdamW(groups)
for g in opt.param_groups:
g["initial_lr"] = g["lr"]
return opt
def lr_multiplier(progress, warmup=0.02, min_ratio=0.1):
if progress < warmup:
return progress / warmup
t = (progress - warmup) / (1.0 - warmup)
return min_ratio + (1.0 - min_ratio) * 0.5 * (1.0 + math.cos(math.pi * t))
def make_loader(data_dir, seq_len, device, batch_size=1):
import glob
import queue
import threading
shards = sorted(glob.glob(os.path.join(data_dir, "*.parquet")))
if not shards:
shards = sorted(glob.glob(os.path.join(data_dir, "*.bin")))
if not shards:
raise FileNotFoundError(f"No parquet or bin shards in {data_dir}")
import numpy as np
def load_fn(path):
data = np.memmap(path, dtype=np.int32, mode="r")
n = (len(data) // (seq_len + 1)) * (seq_len + 1)
return torch.from_numpy(data[:n].copy()).long().view(-1, seq_len + 1)
else:
import pyarrow.parquet as pq
def load_fn(path):
tbl = pq.read_table(path, columns=["tokens"])
tokens = torch.tensor(tbl["tokens"].to_pylist()[0], dtype=torch.long)
n = (len(tokens) // (seq_len + 1)) * (seq_len + 1)
return tokens[:n].view(-1, seq_len + 1)
buf: queue.Queue = queue.Queue(maxsize=4)
def _producer():
ep = 1
while True:
for shard in shards:
seqs = load_fn(shard)
if len(seqs) == 0:
continue
idx = torch.randperm(len(seqs))
batch = []
for i in idx:
batch.append(seqs[i])
if len(batch) == batch_size:
chunk = torch.stack(batch)
buf.put((chunk[:, :-1], chunk[:, 1:], ep))
batch = []
ep += 1
threading.Thread(target=_producer, daemon=True).start()
while True:
x, y, ep = buf.get()
yield x.to(device), y.to(device), ep
def train_variant(
chunk: int,
args,
out_dir: Path,
device: str,
is_master: bool,
use_ddp: bool,
local_rank: int,
):
log.info(f"=== Training gla_chunk={chunk} ===")
cfg = make_124m_config(chunk)
model = CPUGPT(cfg).to(device)
if use_ddp:
model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank])
opt = build_optimizer(model, cfg)
if is_master:
raw = model.module if use_ddp else model
log.info(
f" params: {raw.param_count() / 1e6:.1f}M seq_len={cfg.seq_len} chunk={chunk}"
)
total_batch = args.device_batch * max(args.num_gpus, 1)
total_steps = max(1, int(args.tokens / (total_batch * cfg.seq_len)))
log.info(
f" steps={total_steps} total_batch={total_batch} tokens≈{total_steps * total_batch * cfg.seq_len / 1e6:.0f}M"
)
loader = make_loader(args.data_dir, cfg.seq_len, device, args.device_batch)
ctx = (
torch.autocast(device_type="cuda", dtype=torch.bfloat16)
if "cuda" in device
else torch.no_grad().__class__()
)
step = 0
t0 = time.perf_counter()
for x, y, _ in loader:
if step >= total_steps:
break
model.train()
progress = step / total_steps
mult = lr_multiplier(progress)
for g in opt.param_groups:
g["lr"] = g["initial_lr"] * mult
with torch.autocast(device_type=device.split(":")[0], dtype=torch.bfloat16):
loss = model(x, y)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
opt.zero_grad(set_to_none=True)
step += 1
if is_master and step % 50 == 0:
elapsed = time.perf_counter() - t0
tps = step * total_batch * cfg.seq_len / elapsed
log.info(
f" chunk={chunk} step={step}/{total_steps} ({100 * step / total_steps:.0f}%) "
f"loss={loss.item():.4f} tok/s={tps:,.0f}"
)
ckpt_dir = out_dir / f"variant_{chunk}" / "ckpt"
ckpt_dir.mkdir(parents=True, exist_ok=True)
if is_master:
raw = model.module if use_ddp else model
torch.save({"step": step, "model": raw.state_dict()}, ckpt_dir / "final.pt")
log.info(f" saved {ckpt_dir}/final.pt")
return model, cfg
_HAY = (
"The forest was quiet except for the occasional rustle of leaves in the breeze. "
"Sunlight filtered through the canopy, casting dappled shadows on the mossy ground. "
"A small stream wound its way between ancient oak trees, its water clear and cold. "
"Birds called to one another across the branches, their songs filling the still air. "
"Somewhere in the distance, a woodpecker drummed steadily against hollow bark. "
"The smell of earth and pine needles rose from the path with each careful step. "
"Nothing moved except the shadows and the water and the swaying tops of the tallest trees. "
) * 300
def eval_needle(model, cfg, device, n_trials=20, seed=42):
import tiktoken
enc = tiktoken.get_encoding("gpt2")
hay_ids = enc.encode_ordinary(_HAY)
rng = random.Random(seed)
ctx_lens = [512, 1024, 2048, 4096, cfg.seq_len]
depths = [0.1, 0.3, 0.5, 0.7, 0.9]
raw = (
model.module
if isinstance(model, nn.parallel.DistributedDataParallel)
else model
)
raw.eval()
results = []
for ctx_len in ctx_lens:
for depth in depths:
n_correct, n_valid = 0, 0
for _ in range(n_trials):
code = str(rng.randint(10000, 99999))
dist_ = str(rng.randint(10000, 99999))
while dist_ == code:
dist_ = str(rng.randint(10000, 99999))
needle_ids = enc.encode_ordinary(
f" The passkey is {code}. Remember this passkey."
)
query_ids = enc.encode_ordinary(" The passkey is")
code_ids = enc.encode_ordinary(" " + code)
dist_ids = enc.encode_ordinary(" " + dist_)
overhead = (
len(needle_ids) + len(query_ids) + max(len(code_ids), len(dist_ids))
)
hay_budget = ctx_len - overhead
if hay_budget < 64:
continue
prefix_len = int(hay_budget * depth)
suffix_len = hay_budget - prefix_len
start = rng.randint(0, max(0, len(hay_ids) - hay_budget - 1))
hay_slice = hay_ids[start : start + hay_budget]
context = (
hay_slice[:prefix_len]
+ needle_ids
+ hay_slice[prefix_len : prefix_len + suffix_len]
+ query_ids
)
def avg_nll(answer_ids):
combined = context + answer_ids
remainder = len(combined) % cfg.gla_chunk
extra = (cfg.gla_chunk - remainder) % cfg.gla_chunk
full = combined + [0] * extra
inp = torch.tensor([full], dtype=torch.long, device=device)
with torch.no_grad():
logits = raw(inp)
s = len(context) - 1
lgt = logits[0, s : s + len(answer_ids)].float()
tgt = torch.tensor(answer_ids, dtype=torch.long, device=device)
return F.cross_entropy(lgt, tgt, reduction="mean").item()
try:
nll_c = avg_nll(code_ids)
nll_d = avg_nll(dist_ids)
n_valid += 1
if nll_c < nll_d:
n_correct += 1
except Exception:
pass
acc = n_correct / n_valid if n_valid else float("nan")
results.append(
{
"ctx_len": ctx_len,
"depth": depth,
"accuracy": acc,
"n_correct": n_correct,
"n_valid": n_valid,
}
)
log.info(
f" [needle] chunk={cfg.gla_chunk} ctx={ctx_len} depth={depth:.1f} "
f"acc={acc:.0%} ({n_correct}/{n_valid})"
)
return results
def eval_throughput(model, cfg, device):
raw = (
model.module
if isinstance(model, nn.parallel.DistributedDataParallel)
else model
)
raw.eval()
results = []
ctx_lens = [512, 1024, 2048, 4096, cfg.seq_len]
for ctx_len in ctx_lens:
padded = math.ceil(ctx_len / cfg.gla_chunk) * cfg.gla_chunk
x = torch.zeros(1, padded, dtype=torch.long, device=device)
for _ in range(2):
with torch.no_grad():
raw(x)
torch.cuda.synchronize() if "cuda" in device else None
t0 = time.perf_counter()
reps = 5
for _ in range(reps):
with torch.no_grad():
raw(x)
torch.cuda.synchronize() if "cuda" in device else None
dt = (time.perf_counter() - t0) / reps
tps = padded / dt
results.append(
{"ctx_len": ctx_len, "tok_per_sec": tps, "ms_per_seq": dt * 1000}
)
log.info(
f" [throughput] chunk={cfg.gla_chunk} ctx={ctx_len}{tps:,.0f} tok/s"
)
return results
def eval_wikitext(model, cfg, device):
import tiktoken
try:
from datasets import load_dataset
try:
ds = load_dataset(
"Salesforce/wikitext",
"wikitext-103-raw-v1",
split="test",
trust_remote_code=True,
)
except Exception:
ds = load_dataset(
"wikitext", "wikitext-103-raw-v1", split="test", trust_remote_code=True
)
text = "\n\n".join(ds["text"])
except Exception:
log.warning(" Could not load WikiText-103; skipping BPB eval")
return {"bpb": float("nan")}
enc = tiktoken.get_encoding("gpt2")
tokens = torch.tensor(enc.encode(text[:4_000_000]), dtype=torch.long)
tokens = tokens[:1_000_000]
raw = (
model.module
if isinstance(model, nn.parallel.DistributedDataParallel)
else model
)
raw.eval()
seq_len = cfg.seq_len
n_chunks = min(50, (len(tokens) - 1) // seq_len)
total_nll, total_tok = 0.0, 0
with torch.no_grad():
for i in range(n_chunks):
x = tokens[i * seq_len : (i + 1) * seq_len].unsqueeze(0).to(device)
y = tokens[i * seq_len + 1 : (i + 1) * seq_len + 1].unsqueeze(0).to(device)
if x.shape[1] < seq_len or y.shape[1] < seq_len:
break
pad = cfg.gla_chunk - (x.shape[1] % cfg.gla_chunk or cfg.gla_chunk)
if pad < cfg.gla_chunk:
x = F.pad(x, (0, pad))
logits = raw(x)
lgt = logits[0, :seq_len].float()
loss = F.cross_entropy(lgt, y[0], reduction="sum")
total_nll += loss.item()
total_tok += seq_len
nats = total_nll / max(total_tok, 1)
bpb = nats / math.log(2)
log.info(f" [wikitext] chunk={cfg.gla_chunk} BPB={bpb:.4f}")
return {"bpb": bpb, "nats": nats, "tokens_evaluated": total_tok}
def make_figures(all_results, out_dir: Path):
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
except ImportError:
log.warning("matplotlib not available; skipping figures")
return
chunks = sorted(all_results.keys())
colors = ["#E84855", "#F4A261", "#2E86AB", "#264653"]
fig, axes = plt.subplots(2, 2, figsize=(14, 9), sharex=True, sharey=True)
axes = axes.flatten()
for idx, chunk in enumerate(chunks):
rows = all_results[chunk]["needle"]
ctx_u = sorted(set(r["ctx_len"] for r in rows))
dep_u = sorted(set(r["depth"] for r in rows))
grid = np.full((len(dep_u), len(ctx_u)), float("nan"))
for r in rows:
ci = ctx_u.index(r["ctx_len"])
di = dep_u.index(r["depth"])
if not (isinstance(r["accuracy"], float) and math.isnan(r["accuracy"])):
grid[di, ci] = r["accuracy"]
ax = axes[idx]
im = ax.imshow(grid, vmin=0, vmax=1, cmap="RdYlGn", aspect="auto")
ax.set_title(f"gla_chunk={chunk}", fontsize=12, fontweight="bold")
ax.set_xticks(range(len(ctx_u)))
ax.set_xticklabels(
[f"{c // 1024}K" if c >= 1024 else str(c) for c in ctx_u], fontsize=9
)
ax.set_yticks(range(len(dep_u)))
ax.set_yticklabels([f"{int(d * 100)}%" for d in dep_u], fontsize=9)
ax.set_xlabel("Context Length", fontsize=10)
ax.set_ylabel("Needle Depth", fontsize=10)
for di in range(len(dep_u)):
for ci in range(len(ctx_u)):
v = grid[di, ci]
if not math.isnan(v):
col = "black" if 0.25 < v < 0.75 else "white"
ax.text(
ci,
di,
f"{v:.0%}",
ha="center",
va="center",
fontsize=9,
color=col,
fontweight="bold",
)
plt.colorbar(im, ax=ax)
fig.suptitle(
"Passkey Retrieval Accuracy by GLA Chunk Size\n(124M FELA, 1B tokens)",
fontsize=14,
)
fig.tight_layout()
fig.savefig(out_dir / "fig_chunk_retrieval.png", dpi=150, bbox_inches="tight")
plt.close(fig)
log.info(" saved fig_chunk_retrieval.png")
fig, ax = plt.subplots(figsize=(8, 5))
for chunk, color in zip(chunks, colors):
rows = all_results[chunk].get("throughput", [])
if not rows:
continue
xs = [r["ctx_len"] for r in rows]
ys = [r["tok_per_sec"] for r in rows]
ax.plot(
xs,
ys,
"o-",
color=color,
linewidth=2.5,
markersize=6,
label=f"chunk={chunk}",
)
ax.set_xscale("log", base=2)
ax.set_xlabel("Context Length (tokens)", fontsize=12)
ax.set_ylabel("Throughput (tok/s)", fontsize=12)
ax.set_title(
"Throughput vs. Context Length\n(124M FELA — chunk size effect)", fontsize=13
)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig(out_dir / "fig_chunk_throughput.png", dpi=150, bbox_inches="tight")
plt.close(fig)
log.info(" saved fig_chunk_throughput.png")
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
acc_hard, bpb_vals = [], []
for chunk in chunks:
needle_rows = all_results[chunk]["needle"]
match = [
r
for r in needle_rows
if r["ctx_len"] == 4096 and abs(r["depth"] - 0.1) < 0.01
]
acc_hard.append(match[0]["accuracy"] if match else float("nan"))
bpb_vals.append(all_results[chunk].get("wikitext", {}).get("bpb", float("nan")))
c_labels = [f"chunk={c}" for c in chunks]
ax1 = axes[0]
bars = ax1.bar(c_labels, acc_hard, color=colors)
ax1.axhline(0.5, color="gray", linestyle="--", alpha=0.7, label="random")
ax1.set_ylim(0, 1)
ax1.set_ylabel("Retrieval Accuracy")
ax1.set_title("Hard case: ctx=4K, depth=10%\n(needle at very start)")
ax1.legend(fontsize=9)
for bar, v in zip(bars, acc_hard):
if not math.isnan(v):
ax1.text(
bar.get_x() + bar.get_width() / 2,
v + 0.02,
f"{v:.0%}",
ha="center",
fontsize=10,
fontweight="bold",
)
ax2 = axes[1]
bars2 = ax2.bar(c_labels, bpb_vals, color=colors)
ax2.set_ylabel("WikiText BPB (lower = better)")
ax2.set_title("Language quality\n(WikiText-103 BPB)")
for bar, v in zip(bars2, bpb_vals):
if not math.isnan(v):
ax2.text(
bar.get_x() + bar.get_width() / 2,
v + 0.005,
f"{v:.3f}",
ha="center",
fontsize=10,
fontweight="bold",
)
fig.suptitle("Chunk Size Tradeoff: Retrieval vs. Quality", fontsize=13)
fig.tight_layout()
fig.savefig(out_dir / "fig_chunk_summary.png", dpi=150, bbox_inches="tight")
plt.close(fig)
log.info(" saved fig_chunk_summary.png")
def main():
parser = argparse.ArgumentParser(description="GLA chunk-size ablation")
parser.add_argument("--data-dir", default="/data/openwebtext")
parser.add_argument(
"--tokens",
type=float,
default=1e9,
help="training tokens per variant (default 1B)",
)
parser.add_argument(
"--device-batch", type=int, default=4, help="per-device batch size (sequences)"
)
parser.add_argument(
"--num-gpus", type=int, default=0, help="override GPU count (0 = auto-detect)"
)
parser.add_argument("--out-dir", default="results/chunk_ablation")
parser.add_argument("--needle-trials", type=int, default=20)
parser.add_argument(
"--smoke",
action="store_true",
help="fast smoke test: 5M tokens, 3 needle trials",
)
parser.add_argument(
"--eval-only",
type=str,
default=None,
help="path to existing results dir — skip training, just eval+figures",
)
parser.add_argument(
"--chunks",
type=int,
nargs="+",
default=CHUNK_VARIANTS,
help="which chunk sizes to run (default: 64 256 512 1024)",
)
args = parser.parse_args()
if args.smoke:
args.tokens = 5e6
args.needle_trials = 3
use_ddp = False
local_rank, global_rank = 0, 0
if "RANK" in os.environ:
dist.init_process_group(backend="nccl")
local_rank = dist.get_rank() % torch.cuda.device_count()
global_rank = dist.get_rank()
use_ddp = True
torch.cuda.set_device(local_rank)
num_gpus = args.num_gpus or (
torch.cuda.device_count() if torch.cuda.is_available() else 0
)
args.num_gpus = max(1, num_gpus)
device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu"
is_master = global_rank == 0
out_dir = Path(args.out_dir)
if is_master:
out_dir.mkdir(parents=True, exist_ok=True)
all_results = {}
for chunk in args.chunks:
variant_dir = out_dir / f"variant_{chunk}"
if is_master:
variant_dir.mkdir(parents=True, exist_ok=True)
model, cfg = train_variant(
chunk, args, out_dir, device, is_master, use_ddp, local_rank
)
if use_ddp:
dist.barrier()
if is_master:
raw = model.module if use_ddp else model
raw.eval()
log.info(f"--- Evaluating chunk={chunk} ---")
needle_res = eval_needle(raw, cfg, device, n_trials=args.needle_trials)
(variant_dir / "needle.json").write_text(json.dumps(needle_res, indent=2))
tput_res = eval_throughput(raw, cfg, device)
(variant_dir / "throughput.json").write_text(json.dumps(tput_res, indent=2))
wiki_res = eval_wikitext(raw, cfg, device)
(variant_dir / "wikitext.json").write_text(json.dumps(wiki_res, indent=2))
all_results[chunk] = {
"needle": needle_res,
"throughput": tput_res,
"wikitext": wiki_res,
"params_M": raw.param_count() / 1e6,
"gla_chunk": chunk,
}
del model
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
if use_ddp:
dist.barrier()
if is_master:
(out_dir / "summary.json").write_text(json.dumps(all_results, indent=2))
log.info(f"Summary written to {out_dir}/summary.json")
make_figures(all_results, out_dir)
print("\n" + "=" * 65)
print("CHUNK ABLATION SUMMARY")
print("=" * 65)
print(
f"{'chunk':>8} {'4K/10% acc':>10} {'4K/50% acc':>10} {'4K/90% acc':>10} {'BPB':>7}"
)
print("-" * 65)
for chunk in sorted(all_results.keys()):
nr = all_results[chunk]["needle"]
def get_acc(ctx, dep):
m = [
r
for r in nr
if r["ctx_len"] == ctx and abs(r["depth"] - dep) < 0.01
]
return m[0]["accuracy"] if m else float("nan")
bpb = all_results[chunk].get("wikitext", {}).get("bpb", float("nan"))
print(
f"{chunk:>8} {get_acc(4096, 0.1):>10.0%} {get_acc(4096, 0.5):>10.0%} "
f"{get_acc(4096, 0.9):>10.0%} {bpb:>7.4f}"
)
print("=" * 65)
if use_ddp:
dist.destroy_process_group()
if __name__ == "__main__":
main()