Metadata-Version: 2.4
Name: melog
Version: 1.5.0
Summary: 轻量级训练监控库：多 GPU 指标合并、控制台实时进度条、Web 可视化
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: fastapi>=0.100
Requires-Dist: uvicorn>=0.23
Requires-Dist: websockets>=11.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: httpx>=0.24; extra == "dev"

﻿<div align="center">
  <img src="assets/logo.svg" alt="Melog" width="340">
</div>

轻量级训练监控库：**多 GPU 指标合并 + 控制台实时进度条 + Web 可视化**。

```text
epoch 3 loss=0.2153 acc=0.8974 lr=8.2e-04 ━━━━━━━━━━━──────────  45.0% [90/200] [0:03<0:04 30.0it/s]
```

## 特性

- **控制台实时进度条**：自研 tqdm（用法与 tqdm.tqdm 一致），`[n/total]` 领先、指标紧随其后实时刷新；终端下自适应列宽渲染——内容不超行、绝不自动换行：进度条吃掉固定段之外的全部剩余列恰好占满整行，剩余不足时收缩到最小宽度，仍放不下的指标以省略号收尾（重定向 / 日志文件无列宽概念，按固定宽度渲染）；进度条与 print 同步镜像到每次会话独立的 `console-<时间戳>.log`（run 目录已有会话产物时新文件自动加序号前缀 `2.`、`3.`……，metrics 日志同理；进度条行就地实时刷新、带时间戳前缀，与终端内容一致，编辑器打开可看到动的进度条）
- **多 GPU 指标合并**：基于 `torch.distributed` all_reduce 跨进程聚合（默认取均值），仅 rank0 记录与展示；未装 torch 自动退化单进程。`StepsBar(reduce=False)` 可关闭合并（训练期间只看 master 实时值，零集合通信）
- **Web 可视化**：FastAPI + WebSocket + ECharts，后台线程运行，实时推送曲线，断线自动重连；支持 `tab` 分区（train / val / test 垂直分块，**各分区 step 独立计数**）与层级命名多系列卡片（多分类逐类曲线一图对比）
- **持久化**：指标写入自研二进制容器（符号表 + varint 增量编码，体积约为 JSONL 的 1/4），每次启动一个带时间戳的会话文件，互不覆盖
- **断点续训**：重跑同一 `log_dir` 自动接续历史曲线；从某个 epoch 重新训练时自动清除上次中断留下的重叠数据，折线不会在 x 轴上回退

## 安装

```bash
pip install melog            # 基础安装，来自 PyPI
```

多 GPU 合并基于 `torch.distributed`，假定环境中已装好 PyTorch；未装 torch 时自动退化单进程。

## 快速开始

```python
import melog
from melog import StepsBar

melog.init("runs/my-exp")            # 日志保存路径；端口缺省自动选空闲端口
                                     # Web 地址启动时自动打印，也可读 melog.current().web_url

for step in StepsBar(range(1000)):   # tqdm 风格：自动推进，无需手动 update
    loss = train_one_step()
    melog.scalar({"loss": loss, "lr": 1e-3})   # 记录 + 刷新进度条指标 + 推送 Web
```

训练期间浏览器打开启动时打印的 Web 地址（即 `melog.current().web_url`）查看实时曲线。

## 全局共享

`melog.init` 是唯一入口，创建的实例自动成为全局活动实例。入口处 `init` 一次，
项目任何地方直接用模块级接口，无需层层传递实例：

```python
import melog

melog.init(log_dir="runs/my-exp")   # 日志保存路径；端口缺省自动选空闲端口

# 任意其他模块中：
import melog
melog.scalar({"loss": 0.5})
melog.image("sample", img)
# 收尾：进程退出时自动完成，无需调用
```

- `log_dir` 末级目录名即项目名（`runs/my-exp` → 项目 `my-exp`），本次运行落在
  `runs/my-exp/<时间戳>/` 下；`project=` 可覆盖项目名
- 最近一次创建的实例即全局活动实例（`melog.current()` 取回），收尾后清空
- 进程退出时经 atexit 自动收尾：落盘剩余指标、定稿进度条、停 Web、还原 print，
  无需任何手动调用
- 模块级 `scalar / image / audio / log / success / error / warn / set_colors / current_bar` 与实例方法等价
- 实例内部有锁，多线程 / 多模块共享安全；多 GPU 约定不变

## 曲线上体现 epoch

本库**按 epoch 组织训练记录**：每个 epoch 的循环必须用 `StepsBar` 包裹并传入
`epoch`，坐标（epoch / step）由它统一管理——`scalar()` / `image()` /
`audio()` 都**没有坐标参数**，记录自动依附当前 epoch 与下一个空槽：

```python
from melog import StepsBar

for epoch in range(epochs):
    for _ in StepsBar(loader, epoch=epoch):        # 行首自动标注 "epoch N"
        loss = train_one_step()
        melog.scalar({"loss": loss, "lr": lr})     # 坐标自动依附当前 epoch
```

- `StepsBar(epoch=...)` 进入进度条即绑定 epoch：epoch 内步数清零、全局 x 从上一位置
  接续；bar 结束后沿用绑定值，直至下一个 epoch
- `step` 为**当前 epoch 内**的记录序号，内部自增（每个 epoch 从 0 重新计步）；
  完全没用 `StepsBar(epoch=...)` 时退化为全局自增 x、不标注 epoch 分界
- 要控制记录粒度（每步 / 每 N 步窗口），调整调用 `scalar()` 的频率即可，无需手动指定坐标
- Web 曲线在每个 epoch 起点画分界虚线（标注 `e0` / `e1` / …），悬浮提示显示 `epoch=N · step X`
- `MetricGroup` 末尾收尾交给 `StepsBar` 的自动记录（见下文），epoch 沿用绑定值

## 控制台消息

print 风格的控制台输出接口：多参数自动转 `str()`、以 `sep` 拼接，签名对齐 `print`
（支持 `sep` / `end` / `flush`）：

```python
melog.log("普通消息", {"k": 1})   # 终端默认色（黑字），无前缀
melog.success("保存完成")         # 绿色 ✔
melog.error("加载失败")           # 红色 ✘
melog.warn("学习率过大")          # 黄色 ⚠
```

实例存活期间（仅 rank0），官方 `print(...)` 会被拦截内部改走 `log()`——普通打印
自动带上图标/配色并同步进 console.log，进程退出收尾后还原原生 print。颜色仅在真实
终端（TTY）启用，重定向 / console.log 始终纯文本。

多 GPU 下日志消息**默认仅 rank0 输出**（与指标记录一致，避免 N 卡重复刷屏）；
调试需要各卡都打印时传 `all_ranks=True`（非主卡仅上终端，不进 console.log——
镜像只在 rank0 挂载）：

```python
melog.log("只有 rank0 打印")                     # 默认：其余卡静默
melog.log("每个卡都打印自己的 local 值", all_ranks=True)  # 调试用
```

## 记录图像与音频

除指标曲线外，Web 端 header 可在 **曲线 / 图像 / 音频** 三个页签间切换。图像与音频用
`image` / `audio` 记录，Web 端按名字建卡片、滑杆按 step 回放（图像点击看原图，
音频在线播放）；文件自动落盘到 `run_dir/media/`，元数据随日志持久化，历史日志加载时
媒体一并恢复：

```python
melog.scalar({"loss": loss})            # 坐标自动依附当前 epoch（StepsBar 绑定）
melog.image("train/sample", img)        # 路径 / PIL / numpy / torch
melog.image("val/sample", img)          # 自动附着最近一次 scalar() 的位置
melog.audio("val/audio", wav, sr=16000) # 路径(wav/mp3/…) / numpy / torch 波形
```

- 图像 / 音频自动附着到**最近一次 `scalar()` 的位置**，不推进计数；
  坐标（epoch / step）由 `StepsBar` 统一管理，接口无坐标参数
- `caption="..."` 可为每条图像 / 音频配一段文字（如样本说明、转写文本），
  显示在卡片上、随滑杆切换；换行会被保留
- 图像：`(H,W)` 灰度或 `(H,W,C)`（C=1/3/4），浮点自动映射 0-255，统一存为 PNG
- 音频：`(N,)` 单声道或 `(N, 声道数)`，浮点按 [-1,1] 裁剪存为 16bit WAV；
  传文件路径则按原格式复制
- 数组编码需要 `pillow`（仅图像）：`pip install pillow`

## 指标计算（多 GPU 自动同步）

内置 `Mean` / `Sum` / `Last` / `Count`，按 epoch 组织在 `MetricGroup` 中使用：

```python
import melog
from melog import Last, Mean, MetricGroup, StepsBar, Sum

melog.init("runs/my-exp")
metrics = MetricGroup({
    "loss": Mean(),      # 各 batch 等权平均
    "acc": Mean(),
    "seen": Sum(),       # 求和
    "lr": Last(),        # 最近一次喂入值
})

for epoch in range(epochs):
    # metrics=... 传入后：每次 feed 自动把本卡本地值写入日志/面板
    # （实时曲线，零通信），epoch 末自动跨 GPU 合并出全局值再记录
    # 一次并 reset 清零。训练曲线（loss / recall 等）无需跨卡合并时传
    # reduce=False：bar 打开期间的所有记录都跳过 all_reduce，只记
    # master 实时值（各 rank 无需在对齐位置调用）；epoch 末只 reset
    for _ in StepsBar(range(steps), epoch=epoch, metrics=metrics, reduce=False):
        metrics.feed(loss=loss, acc=acc, seen=batch_size, lr=lr)
    # feed(..., write=False) 只累积内存（如验证集不想逐 batch 写曲线）；
    # epoch 末仍自动跨 GPU 合并记录 + reset，无需手动 scalar（默认
    # reduce=True，对验证集测试集结果多卡合并）
```
- `Mean` 默认按 StepsBar 自动识别的**批次样本数**精确平均（feed 无需传
  batch_size；各 batch 等样本数时即等权）；多 GPU 下合并为全局样本平均，
  而非"各卡平均值的平均"
- 批次样本数自动识别（tensor / numpy 的 shape[0]、字典、列表 / 元组递归）；
  识别失败（如迭代 range）回退等权平均并警告一次。需手动指定时传**元组**
  `(值, 观测数)`：`metrics.feed(loss=(loss, token_num))`，显式值优先
- `melog.scalar(metrics)` 随时可落盘当前累计值；**必须算完一个 epoch 才有意义的指标**，
  在 epoch 末统一记录一次即可（交给 StepsBar 自动执行，或手动调用）
- 跨 GPU 合并是集合操作：**所有 rank 必须以相同顺序执行**，返回值各 rank 一致；单进程自动直通
- 实时 + 精确一步到位：`StepsBar(loader, epoch=e, metrics=metrics)`——
  每次 feed 自动把本卡本地值写入日志/面板（零通信，仅 rank0 落盘），bar 同步实时显示；
  迭代自然结束时自动 gather 所有 rank 合并出**全局值**再记录一次（提前 break / 抛异常不触发，
  以免各 rank 在 all_gather 处互相等待；所有 rank 都会执行，落盘仅 rank0）。
  `feed(..., write=False)` 关闭逐 batch 实时写入（如验证集场景），epoch 末
  仍自动合并记录，无需手动 scalar
- **bar 跑完自动打印最终结果**：`StepsBar(metrics=...)` 迭代自然结束时把
  本 bar 监控的 metrics 最终结果打印到控制台（bar 行上方一行）——
  `reduce=True` 打印**跨卡合并后的结果**（与落盘值一致），`reduce=False`
  打印本卡本地累计值（重置前）；未观测到的指标（NaN）与非数值结果不
  打印，仅 rank0 输出；`print_result=False` 关闭
- `on_end=...`：epoch 末自动记录完成后触发的回调，参数为**跨 GPU 合并后的
  指标字典**（与落盘值一致，未观测到的指标为 NaN）。典型用途如按验证指标
  保存 checkpoint。需配合 `metrics` 使用且 `reduce=True`（reduce=False 时
  epoch 末不做合并记录、本回调不触发，同时传入会被拒绝）；所有 rank 都会
  执行（合并是集合操作），各卡收到的值一致，仅想主卡执行时在回调内自行
  判断 rank；提前 break / 抛异常不触发：

  ```python
  for _ in StepsBar(val_loader, epoch=epoch, metrics=val_metrics,
                    on_end=lambda m: save_if_best(m["acc"])):
      val_metrics.feed(args=(logits, labels), loss=loss, write=False)
  ```

### tab 面板分区（train / val / test）与 step 隔离

`StepsBar` 传 `tab`（或 `melog.scalar(..., tab=...)`）把记录分到面板独立
分区——记录名自动加前缀（`train/loss`），Web 面板按**显式声明的分区**把
卡片分到独立区块、垂直排列（不靠命名猜测）；分区内仍按指标名分卡，
`recall/class_0` 式逐类命名照常合并为一张多系列卡片。分区随日志持久化，
历史日志重新加载时一并恢复。

**各分区的 step 独立计数、互不影响**：train 的 40 步用 train 分区的
x 0..39，val 的记录从 val 分区的 x 0 起（写 val 不推进 train 的步数），
面板左侧切换分区后各自独立展示：

```python
def make_metrics():
    return MetricGroup({
        "loss": Mean(),
        "lr": Last(),
        **{f"recall/class_{c}": Recall(num_classes=K, class_index=c) for c in range(K)},
    })

train_metrics = make_metrics()   # → train/loss, train/lr, train/recall/class_0...
val_metrics   = make_metrics()   # 同一套定义挂到不同分区

for epoch in range(EPOCHS):
    # 训练分区：只看 master 实时指标，reduce=False 跳过跨卡合并
    for _ in StepsBar(train_loader, epoch=epoch, tab="train",
                      metrics=train_metrics, reduce=False):
        train_metrics.feed(...)
    # 验证分区：结果需对验证集跨卡合并（默认 reduce=True）
    for _ in StepsBar(val_loader, epoch=epoch, tab="val", metrics=val_metrics):
        val_metrics.feed(write=False)   # epoch 末自动合并记录 + reset
```

- 分区是 StepsBar 的 `tab=...` 显式属性而非命名约定：不传 `tab` 时记录进
  默认序列（`loss`、`recall/class_0` 等原有命名分组不受影响）
- StepsBar 声明了 `tab` 时，bar 内不带 `tab` 的 `melog.scalar(...)` 记录
  也归入该分区
- 完整示例见 `examples/tab_demo.py`

### feed 如何分发观测

`metrics.feed(args=..., **scalars)` 把一个 batch 的观测一次喂入，两类指标
**分开传、各取所需**。以

```python
metrics = MetricGroup({"loss": Mean(), "macc": MaskedAcc()})
metrics.feed(args={"logits": logits, "labels": labels, "mask": mask},
             loss=(loss, batch_size))
```

为例，一次 feed 内部的流转：

- **`args=`：观测型指标**（如 `"macc"` 与所有内置分类指标）的观测，
  单独成组——**字典**按键名对应各指标 `compute` / `prepare` 的形参
  （推荐，形参多时更可读），自动分发给形参名匹配的指标，多余的键忽略；
  **元组**按位置喂给未被注册名喂入的指标。
  缺少必需形参才抛 `KeyError`。
- **`**scalars`：按注册名喂入的指标**（`Mean` / `Sum` / `Last` / `Count`，如 `"loss"`）：
  按**注册名**找同名键——取出 `loss=(loss, batch_size)`；是元组就展开为
  `feed(loss, batch_size)` 加权累积，普通数值则等权。本 batch 没有同名键就跳过
  （不累积也不报错）。

一句话：**观测型指标的观测放 `args`，标量指标按注册名"点名取值"**。两类规则
互不干扰，所以同一个 feed 调用可以同时喂两类指标；无主的多余观测两边都不收。

单独使用某个指标时规则一致：标量指标位置喂入 `Mean().feed(value, count)`；
观测型指标具名或位置均可 `MaskedAcc().feed(logits=..., labels=..., mask=...)`，
框架同样按 `compute` / `prepare` 形参名组装。

### 分类指标

内置 `Accuracy` / `Precision` / `Recall` / `F1` / `ConfusionMatrix`，接口与基础指标一致，
`feed(logits, labels)` 直接接收模型输出与标签：

```python
from melog import Accuracy, F1, MetricGroup, Mean, Precision

metrics = MetricGroup({
    "loss": Mean(),
    "acc": Accuracy(),                 # 二分类：一维得分按阈值 0.5 判定
    "acc5": Accuracy(topk=5),          # top-5 准确率（多分类）
    "f1": F1(num_classes=10),          # 多分类：二维 (N, K) logits 按行 argmax
})

# 验证集：write=False 不逐 batch 写曲线，epoch 末自动跨 GPU 合并记录
for logits, labels in StepsBar(val_loader, epoch=epoch, metrics=val_metrics):
    # feed：观测型指标的观测放 args（元组按位置 / 字典按键名），
    # 标量指标按注册名喂入（loss 自动按批次样本数平均）
    val_metrics.feed(args=(logits, labels), loss=loss, write=False)
# 无需手动 scalar：StepsBar 结束时自动合并记录并 reset
```

- `Accuracy(topk=k)`：真实类别在前 k 个预测中即算正确
- `Precision / Recall / F1` 的 `average`：`None`（二分类=正类，多分类=macro）/ `"macro"` / `"micro"` / `"weighted"`
- `ConfusionMatrix` 的 `compute()` 返回矩阵（行=真实、列=预测），适合直接读取而非画曲线
- 预测规则由 `preds_from_logits` 实现，可传 `predictor=` 替换（如多标签、分割等自定义转换）

### 自定义指标

统一继承 `Metric`，按指标何时出值选择实现方式：

**实时指标——只实现 `compute()`**：每次喂入立即用本批观测算出指标值，
形参名和个数完全由你定义，框架按形参名自动从 `feed()` 的观测中取值回调；
各 batch 结果按各自实际的样本数加权平均、跨 GPU 合并，全部由框架完成：

```python
from melog import Metric

class MaskedAcc(Metric):
    """需要几个参数就声明几个，logits/labels 仅为示例。"""
    def compute(self, logits, labels, mask):
        hits = ((logits.argmax(-1) == labels) & mask).sum()
        n = mask.sum()
        return (hits / n, n)          # 返回 (值, 观测数)：按样本数平均出全局结果

# 训练循环里：位置或具名喂入均可，多余观测自动忽略
metric.feed(logits, labels, mask)
metric.feed(logits=logits, labels=labels, mask=mask)
```

- `compute` 返回 `(值, 观测数)` 元组：各 batch 按观测数（如样本数）平均（样本数不同时务必带上）；
  只返回 float 时各 batch 等权平均
- 组合使用时交给 `MetricGroup.feed(...)` 统一分发：

```python
metrics = MetricGroup({"loss": Mean(), "macc": MaskedAcc()})

# 每个 batch：feed 把观测累积进各指标的内存状态并自动记录本卡实时值
# （观测型指标观测放 args，标量指标按注册名，Mean 自动按批次样本数平均）
for logits, labels, mask in StepsBar(loader, epoch=epoch, metrics=metrics):
    metrics.feed(args={"logits": logits, "labels": labels, "mask": mask},
                 loss=loss)

# epoch 末无需任何手动调用：StepsBar 结束时自动跨 GPU 合并记录 + reset
```

不用 StepsBar 包裹时（如独立验证脚本）才需要手动落盘：
`melog.scalar(metrics)` 跨 GPU 合并记录，`metrics.reset()` 清零开启下一轮。

**epoch 级指标——加实现 `prepare()`**：全局结果无法由各 batch 值按样本数加权平均还原时
（如 macro F1、AUC），每次喂入先用 `prepare()`"备料"——接收同样的观测，
返回本批次贡献的增量（数值 / 字典 / 列表），框架自动累积并跨 GPU 合并
（数值求和、字典按键合并、列表拼接）；epoch 末把合并后的总量交给
`compute()` 算出全局结果：

```python
from melog import Metric

class F1(Metric):
    """epoch 末才能计算的指标：累积混淆计数，末尾统一算。"""
    def prepare(self, tp, fp, fn):      # 每个 batch：本批次贡献的计数
        return {"tp": tp, "fp": fp, "fn": fn}

    def compute(self, tp, fp, fn):      # epoch 末：由总量算出全局值
        return 2 * tp / (2 * tp + fp + fn) if tp + fp + fn else float("nan")

# 使用（通常放进 MetricGroup / StepsBar 自动记录）：
f1 = F1()
f1.feed(tp=2, fp=1, fn=0)   # 位置或具名喂入均可
f1.result()                  # 跨 GPU 合并并计算（单进程直通）
```

## 多 GPU

代码无需修改：按你的分布式训练方式（`torch.distributed` 初始化完成），
每个 rank 进程各自跑同一份 melog 代码，`melog.init` 后库自动感知
分布式环境（未装 torch 或单进程运行则一切退化为本地直通，行为完全一致）。

自定义 `Metric` 时**多卡对你透明，不需要写任何分布式代码**。`compute()`
拿到的永远是框架合并好的**全量**，合并规则按指标类型：

**实时指标（只实现 `compute`）**——各卡的本批值按各自实际的样本数加权平均：

```text
全局值 = Σ(各卡每批的 值 × 该批样本数) / Σ(各卡每批的 样本数)
```

即框架把每卡的 `(值, 观测数)` 对累加后相除——等价于把所有卡的样本
放到一起算，而非"各卡平均值的平均"。

**epoch 级指标（`prepare` + `compute`）**——各卡 `prepare()` 返回的
增量先在本地逐 batch 累积，epoch 末把各卡的本地增量跨卡合并，规则只有
三条（可递归组合）：

| 增量形态 | 合并规则 | 例 |
|---|---|---|
| 数值 | 求和 | `{"tp": 3}` + `{"tp": 2}` → `tp=5` |
| 字典 | 按键递归合并（值按同规则） | 两卡各自 `{"(1,1)": 2, "(1,0)": 1}` 与 `{"(1,1)": 1}` → `{"(1,1)": 3, "(1,0)": 1}` |
| 列表 / 元组 | 拼接 | 两卡各 64 个 `(得分, 标签)` 对 → 128 个 |

三条规则都与合并次序无关（求和与拼接可结合），所以"逐 batch 累积"和
"跨卡合并"用的是同一套规则；epoch 末合并后的总量按 `compute()` 的
形参名传入（字符串键字典展开为关键字参数，其余作为单个位置参数）。

由此带来的性质：

- 不要求各卡数据同构：各卡观测到的类别、样本数可以不同，计数按
  键自动对齐；逐样本对自动拼接，结果与单卡全量计算一致。
- 唯一的纪律：**所有 rank 以相同顺序喂入**（正常训练代码天然满足）。
  记录交给 `StepsBar` / `MetricGroup` 自动完成即自动满足；手动
  `melog.scalar(...)` 时所有 rank 执行同一位置即可。
- 验证正确性最简单的办法：单进程跑一遍的 `result()` 应等于多卡
  全局值（合并规则保证）。

## API

### `melog.init(...)`

| 参数 | 默认 | 说明 |
|---|---|---|
| `log_dir` | `./melog_runs` | 日志保存路径（即 run 目录，日志直接落在其中）；重跑同一目录即断点续训 |
| `web_port` | 随机空闲端口 | Web 监听端口（`web_host` 默认 `127.0.0.1`，地址启动时自动打印，也可读 `melog.current().web_url`） |
| `enable_web` | `True` | 启动 Web 服务（仅 rank0） |
| `enable_progress` | `True` | 启用控制台进度条 |
| `reduce_op` | `"mean"` | 多 GPU 合并方式 |
| `flush_every` | `1` | 每 N 次 scalar 落盘一次 |
| `project` | `log_dir` 末级目录名 | 覆盖项目名 |

### 主要方法

- `scalar(metrics, advance=0)` — 记录一批指标（dict 或 MetricGroup，后者跨 GPU 合并由内部完成）；坐标由 `StepsBar` 自动管理（epoch 绑定 + 内部计步），调用频率即记录粒度（见上文）
- `MetricGroup(metrics, category=None)` — 具名指标集合（`from melog import MetricGroup`），`feed(...)` 统一分发观测、跨 GPU 合并；`category=...` 传大类别（train / val / test），Web 面板按类别垂直分区（见上文）
- `image(name, data, caption=None)` / `audio(name, data, sr=22050, ...)` — 记录图像 / 音频，自动附着最近一次记录位置，Web 端页签展示（见上文）
- `StepsBar(iterable, epoch=None, tab=None, metrics=None, reduce=True, on_end=None)` — tqdm 风格训练进度条（`from melog import StepsBar`，模块级 `melog.stepsbar(...)` 等价），**epoch 循环必须用它包裹**：包裹可迭代对象即自动推进，`scalar()` 指标实时显示在条上；`epoch=...` 绑定当前 (分区, epoch) 并统一管理坐标；`tab=...` 声明面板分区（各分区 step 独立计数）；`metrics=...` 传入 MetricGroup 时 bar 实时显示本卡本地值（feed 零通信刷新），迭代自然结束自动 gather 全局值合并记录并重置组内指标（`reduce=False` 只记 master 实时值、不做合并；提前 break / 异常不触发）；`on_end=...` 在合并记录后收到合并后的指标字典（见上文）
- 允许嵌套（如训练 bar 内嵌验证 bar）：内部以栈管理，`current_bar()` 返回栈顶即当前环境；`scalar()` 的 postfix 与 `advance` 自动作用于栈顶，下层 bar 暂停渲染（数据照常累计），栈顶关闭后自动恢复下层渲染；提前 break / 抛异常时 bar 自动出栈（如需立即定稿可 `close()` 或用 with），否则等引用释放时兜底
- `current_bar()` — 当前栈顶进度条（无打开的 bar 时 `None`）；深层函数需要手动推进 / 读数 / 写 postfix 时取它，免层层传参
- `log / success / error / warn` — print 风格控制台消息（`[MM-DD HH:MM:SS][文件名]` 前缀 + 图标 + 彩色文字），`print` 被拦截改走 `log()`；多 GPU 下默认仅 rank0 输出，`all_ranks=True` 时各卡都输出（见上文）
- 收尾无需手动调用：进程退出时经 atexit 自动落盘剩余指标、定稿进度条、停 Web、还原 print
- 全局共享：`melog.init(...)` 创建活动实例后，模块级 `melog.scalar(...)` 等可在任意位置直接调用（见上文）

进度条显示的指标可通过环境变量 `MELOG_DISABLE_PROGRESS=1` 全局关闭。

## CLI 快速查看

安装后可直接用命令行查看历史日志，自动打开浏览器：

```bash
melog F:/runs/exp1/metrics-20260903_101010.melog  # 指定会话日志（自动合并同目录全部会话）
melog F:/runs/exp1                 # 指定 run 目录（合并其全部会话文件）
melog                              # 缺省在 ./melog_runs 中查找
melog F:/runs/exp1 --port 9000 --no-browser  # 自定义端口 / 不开浏览器
```

## 开发

```bash
pip install -e ".[dev]"
pytest tests -q
```
