Download inference_code/inference/pruning/collector.py from Agnes-AI/Agnes-Video-2.0-MoE: direct link, hf CLI and curl.
- Browser
- Download file 21.8 kB
-
https://huggingface.co/Agnes-AI/Agnes-Video-2.0-MoE/resolve/main/inference_code/inference/pruning/collector.py
- Command line
-
hf download hf://Agnes-AI/Agnes-Video-2.0-MoE/inference_code/inference/pruning/collector.py
-
curl -L -o collector.py https://huggingface.co/Agnes-AI/Agnes-Video-2.0-MoE/resolve/main/inference_code/inference/pruning/collector.py
21.8 kB
| """Accumulate TENP projection (and energy control) on routed tokens. No shared expert.""" | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| import torch | |
| from .score import PROJ_EPS, gated_energy, gated_signed_projection | |
| from .temporal import DEFAULT_NUM_BINS, timestep_to_bin | |
| VIDEO, AUDIO, TEXT = 0, 1, 2 | |
| VALID_MODALITIES = (VIDEO, AUDIO, TEXT) | |
| DEFAULT_PERTURB_RATIO = 0.05 | |
| DEFAULT_PERTURB_REPEATS = 3 | |
| DEFAULT_TOKEN_CACHE_PER_BIN = 0 | |
| def _as_2d_table(t: torch.Tensor, d_int: int) -> torch.Tensor: | |
| if t.ndim == 2 and t.shape[0] == 3: | |
| return t | |
| raise ValueError(f"expected [3, d_int={d_int}] table, got {tuple(t.shape)}") | |
| class ExpertStats: | |
| projection: torch.Tensor | |
| energy: torch.Tensor | |
| gram: torch.Tensor | None = None | |
| n_tokens: int = 0 | |
| n_tokens_mod: torch.Tensor = None | |
| projection_bin: torch.Tensor | None = None | |
| energy_bin: torch.Tensor | None = None | |
| n_tokens_bin: torch.Tensor | None = None | |
| n_tokens_mod_bin: torch.Tensor | None = None | |
| sensitivity_sum: torch.Tensor | None = None | |
| sensitivity_count: torch.Tensor | None = None | |
| rel_abs_z: torch.Tensor | None = None | |
| token_cache_per_bin: int = 0 | |
| cache_z: list | None = None | |
| cache_y: list | None = None | |
| cache_gate: list | None = None | |
| cache_mod: list | None = None | |
| _cache_seen: list = field(default_factory=list) | |
| def num_bins(self) -> int: | |
| if self.projection_bin is None: | |
| return 1 | |
| return int(self.projection_bin.shape[0]) | |
| def zeros( | |
| cls, | |
| d_int: int, | |
| device, | |
| num_bins: int = DEFAULT_NUM_BINS, | |
| collect_gram: bool = True, | |
| token_cache_per_bin: int = DEFAULT_TOKEN_CACHE_PER_BIN, | |
| ) -> "ExpertStats": | |
| k = max(int(num_bins), 1) | |
| cap = max(int(token_cache_per_bin), 0) | |
| gram = ( | |
| torch.zeros(d_int, d_int, device=device, dtype=torch.float32) | |
| if collect_gram | |
| else None | |
| ) | |
| return cls( | |
| projection=torch.zeros(3, d_int, device=device, dtype=torch.float32), | |
| energy=torch.zeros(3, d_int, device=device, dtype=torch.float32), | |
| gram=gram, | |
| n_tokens_mod=torch.zeros(3, device=device, dtype=torch.long), | |
| projection_bin=torch.zeros(k, 3, d_int, device=device, dtype=torch.float32), | |
| energy_bin=torch.zeros(k, 3, d_int, device=device, dtype=torch.float32), | |
| n_tokens_bin=torch.zeros(k, device=device, dtype=torch.long), | |
| n_tokens_mod_bin=torch.zeros(k, 3, device=device, dtype=torch.long), | |
| sensitivity_sum=torch.zeros(k, device=device, dtype=torch.float32), | |
| sensitivity_count=torch.zeros(k, device=device, dtype=torch.float32), | |
| rel_abs_z=torch.zeros(d_int, device=device, dtype=torch.float32), | |
| token_cache_per_bin=cap, | |
| cache_z=[None] * k if cap else None, | |
| cache_y=[None] * k if cap else None, | |
| cache_gate=[None] * k if cap else None, | |
| _cache_seen=[0] * k if cap else [], | |
| ) | |
| def _ensure_bins(self, d_int: int) -> None: | |
| k = self.num_bins | |
| device = self.projection.device | |
| if self.projection_bin is None: | |
| self.projection_bin = torch.zeros(k, 3, d_int, device=device, dtype=torch.float32) | |
| self.projection_bin[0] = self.projection | |
| if self.energy_bin is None: | |
| self.energy_bin = torch.zeros(k, 3, d_int, device=device, dtype=torch.float32) | |
| self.energy_bin[0] = self.energy | |
| if self.n_tokens_bin is None: | |
| self.n_tokens_bin = torch.zeros(k, device=device, dtype=torch.long) | |
| self.n_tokens_bin[0] = int(self.n_tokens) | |
| if self.n_tokens_mod_bin is None: | |
| self.n_tokens_mod_bin = torch.zeros(k, 3, device=device, dtype=torch.long) | |
| self.n_tokens_mod_bin[0] = self.n_tokens_mod | |
| if self.sensitivity_sum is None: | |
| self.sensitivity_sum = torch.zeros(k, device=device, dtype=torch.float32) | |
| if self.sensitivity_count is None: | |
| self.sensitivity_count = torch.zeros(k, device=device, dtype=torch.float32) | |
| if self.rel_abs_z is None: | |
| self.rel_abs_z = torch.zeros(d_int, device=device, dtype=torch.float32) | |
| def sensitivity(self, eps: float = 1e-8) -> torch.Tensor: | |
| self._ensure_bins(int(self.projection.shape[-1])) | |
| return self.sensitivity_sum / self.sensitivity_count.clamp_min(eps) | |
| def update( | |
| self, | |
| z: torch.Tensor, | |
| y: torch.Tensor, | |
| gate: torch.Tensor, | |
| modality: torch.Tensor, | |
| W_down: torch.Tensor, | |
| bin_id: int = 0, | |
| collect_sensitivity: bool = False, | |
| perturb_ratio: float = DEFAULT_PERTURB_RATIO, | |
| perturb_repeats: int = DEFAULT_PERTURB_REPEATS, | |
| generator: torch.Generator | None = None, | |
| ) -> None: | |
| valid = (modality >= 0) & (modality <= TEXT) | |
| if int(valid.sum()) == 0: | |
| return | |
| z = z[valid] | |
| y = y[valid] | |
| gate = gate[valid] | |
| modality = modality[valid] | |
| q = gated_signed_projection(z, y, gate, W_down) | |
| e = gated_energy(z, gate) | |
| d_int = int(z.shape[-1]) | |
| self._ensure_bins(d_int) | |
| k = self.num_bins | |
| b = min(max(int(bin_id), 0), k - 1) | |
| if self.gram is not None: | |
| z_g = z.float() * gate.float().reshape(-1, 1) | |
| self.gram.add_(z_g.T @ z_g) | |
| n = int(q.shape[0]) | |
| self.n_tokens += n | |
| self.n_tokens_bin[b] += n | |
| y_norm = y.float().norm(dim=-1, keepdim=True).clamp_min(PROJ_EPS) | |
| self.rel_abs_z.add_((z.float().abs() / y_norm).sum(dim=0)) | |
| for m in VALID_MODALITIES: | |
| mask = modality == m | |
| n_m = int(mask.sum()) | |
| if n_m == 0: | |
| continue | |
| self.n_tokens_mod[m] += n_m | |
| self.n_tokens_mod_bin[b, m] += n_m | |
| q_sum = q[mask].sum(dim=0) | |
| e_sum = e[mask].sum(dim=0) | |
| self.projection[m] += q_sum | |
| self.energy[m] += e_sum | |
| self.projection_bin[b, m] += q_sum | |
| self.energy_bin[b, m] += e_sum | |
| if collect_sensitivity: | |
| d = perturbation_sensitivity( | |
| z, y, W_down, perturb_ratio, perturb_repeats, generator | |
| ) | |
| self.sensitivity_sum[b] += float(d) * n | |
| self.sensitivity_count[b] += n | |
| self._maybe_cache(z, y, gate, modality, b) | |
| def _maybe_cache( | |
| self, | |
| z: torch.Tensor, | |
| y: torch.Tensor, | |
| gate: torch.Tensor, | |
| modality: torch.Tensor, | |
| bin_id: int, | |
| ) -> None: | |
| """Reservoir of `token_cache_per_bin` tokens per (bin, modality). | |
| Per-modality reservoirs keep enough audio/text tokens (they are ~5% of routed | |
| tokens) to measure per-modality output error after pruning. `cache_mod[bin]` | |
| holds the modality id aligned with `cache_z[bin]`. Tensors are stored fp16. | |
| """ | |
| cap = int(self.token_cache_per_bin) | |
| if cap <= 0: | |
| return | |
| k = self.num_bins | |
| if self.cache_z is None: | |
| self.cache_z = [None] * k | |
| self.cache_y = [None] * k | |
| self.cache_gate = [None] * k | |
| self.cache_mod = [None] * k | |
| self._cache_seen = [[0, 0, 0] for _ in range(k)] | |
| if self.cache_mod is None: | |
| self.cache_mod = [None] * k | |
| if not isinstance(self._cache_seen[bin_id], list): | |
| self._cache_seen[bin_id] = [int(self._cache_seen[bin_id]), 0, 0] | |
| z_all = z.detach().to(torch.float16).cpu() | |
| y_all = y.detach().to(torch.float16).cpu() | |
| g_all = gate.detach().float().reshape(-1).cpu() | |
| m_all = modality.detach().to(torch.int8).reshape(-1).cpu() | |
| cur_z = self.cache_z[bin_id] | |
| cur_mod = self.cache_mod[bin_id] | |
| if cur_z is not None and cur_mod is None: | |
| cur_mod = torch.full((int(cur_z.shape[0]),), -1, dtype=torch.int8) | |
| parts_z, parts_y, parts_g, parts_m = [], [], [], [] | |
| for m in VALID_MODALITIES: | |
| sel = (m_all == m).nonzero(as_tuple=False).reshape(-1) | |
| n = int(sel.numel()) | |
| if cur_z is not None: | |
| keep_m = (cur_mod == m).nonzero(as_tuple=False).reshape(-1) | |
| z_m, y_m, g_m = cur_z[keep_m], self.cache_y[bin_id][keep_m], self.cache_gate[bin_id][keep_m] | |
| else: | |
| z_m = torch.empty(0, int(z_all.shape[-1]), dtype=torch.float16) | |
| y_m = torch.empty(0, int(y_all.shape[-1]), dtype=torch.float16) | |
| g_m = torch.empty(0, dtype=torch.float32) | |
| seen = int(self._cache_seen[bin_id][m]) | |
| if n > 0: | |
| filled = int(z_m.shape[0]) | |
| if filled < cap: | |
| take = min(n, cap - filled) | |
| z_m = torch.cat([z_m, z_all[sel[:take]]], dim=0) | |
| y_m = torch.cat([y_m, y_all[sel[:take]]], dim=0) | |
| g_m = torch.cat([g_m, g_all[sel[:take]]], dim=0) | |
| sel = sel[take:] | |
| seen += take | |
| rem = int(sel.numel()) | |
| if rem > 0: | |
| # Reservoir sampling for the remainder: token i (1-based within this | |
| # call) replaces slot r ~ U[0, seen+i) if r < cap. | |
| counts = torch.arange(1, rem + 1, dtype=torch.float64) + float(seen) | |
| r = (torch.rand(rem, dtype=torch.float64) * counts).long() | |
| hit = (r < cap).nonzero(as_tuple=False).reshape(-1) | |
| for i in hit.tolist(): # sequential so later hits overwrite earlier | |
| j = int(sel[i]) | |
| slot = int(r[i]) | |
| z_m[slot] = z_all[j] | |
| y_m[slot] = y_all[j] | |
| g_m[slot] = g_all[j] | |
| seen += rem | |
| self._cache_seen[bin_id][m] = seen | |
| parts_z.append(z_m) | |
| parts_y.append(y_m) | |
| parts_g.append(g_m) | |
| parts_m.append(torch.full((int(z_m.shape[0]),), int(m), dtype=torch.int8)) | |
| self.cache_z[bin_id] = torch.cat(parts_z, dim=0) | |
| self.cache_y[bin_id] = torch.cat(parts_y, dim=0) | |
| self.cache_gate[bin_id] = torch.cat(parts_g, dim=0) | |
| self.cache_mod[bin_id] = torch.cat(parts_m, dim=0) | |
| def state_dict(self) -> dict: | |
| def _cpu_list(items): | |
| if items is None: | |
| return None | |
| out = [] | |
| for t in items: | |
| out.append(None if t is None else t.detach().cpu()) | |
| return out | |
| return { | |
| "projection": self.projection.detach().cpu(), | |
| "energy": self.energy.detach().cpu(), | |
| "gram": None if self.gram is None else self.gram.detach().cpu(), | |
| "n_tokens": int(self.n_tokens), | |
| "n_tokens_mod": self.n_tokens_mod.detach().cpu(), | |
| "projection_bin": None if self.projection_bin is None else self.projection_bin.detach().cpu(), | |
| "energy_bin": None if self.energy_bin is None else self.energy_bin.detach().cpu(), | |
| "n_tokens_bin": None if self.n_tokens_bin is None else self.n_tokens_bin.detach().cpu(), | |
| "n_tokens_mod_bin": None if self.n_tokens_mod_bin is None else self.n_tokens_mod_bin.detach().cpu(), | |
| "sensitivity_sum": None if self.sensitivity_sum is None else self.sensitivity_sum.detach().cpu(), | |
| "sensitivity_count": None if self.sensitivity_count is None else self.sensitivity_count.detach().cpu(), | |
| "rel_abs_z": None if self.rel_abs_z is None else self.rel_abs_z.detach().cpu(), | |
| "token_cache_per_bin": int(self.token_cache_per_bin), | |
| "cache_z": _cpu_list(self.cache_z), | |
| "cache_y": _cpu_list(self.cache_y), | |
| "cache_gate": _cpu_list(self.cache_gate), | |
| "cache_mod": _cpu_list(self.cache_mod), | |
| } | |
| def from_state(cls, payload: dict, device="cpu") -> "ExpertStats": | |
| projection = payload["projection"].to(device=device, dtype=torch.float32) | |
| energy = payload["energy"].to(device=device, dtype=torch.float32) | |
| d_int = int(projection.shape[-1]) | |
| stats = cls( | |
| projection=_as_2d_table(projection, d_int), | |
| energy=_as_2d_table(energy, d_int), | |
| gram=( | |
| None | |
| if payload.get("gram") is None | |
| else payload["gram"].to(device=device, dtype=torch.float32) | |
| ), | |
| n_tokens=int(payload["n_tokens"]), | |
| n_tokens_mod=payload["n_tokens_mod"].to(device=device, dtype=torch.long), | |
| ) | |
| proj_bin = payload.get("projection_bin") | |
| if proj_bin is None: | |
| stats.projection_bin = stats.projection.unsqueeze(0).clone() | |
| stats.energy_bin = stats.energy.unsqueeze(0).clone() | |
| stats.n_tokens_bin = torch.tensor( | |
| [stats.n_tokens], device=device, dtype=torch.long | |
| ) | |
| stats.n_tokens_mod_bin = stats.n_tokens_mod.unsqueeze(0).clone() | |
| stats.sensitivity_sum = torch.zeros(1, device=device, dtype=torch.float32) | |
| stats.sensitivity_count = torch.zeros(1, device=device, dtype=torch.float32) | |
| else: | |
| stats.projection_bin = proj_bin.to(device=device, dtype=torch.float32) | |
| energy_bin = payload.get("energy_bin") | |
| stats.energy_bin = ( | |
| stats.energy.unsqueeze(0).clone() | |
| if energy_bin is None | |
| else energy_bin.to(device=device, dtype=torch.float32) | |
| ) | |
| n_bin = payload.get("n_tokens_bin") | |
| k = int(stats.projection_bin.shape[0]) | |
| stats.n_tokens_bin = ( | |
| torch.zeros(k, device=device, dtype=torch.long) | |
| if n_bin is None | |
| else n_bin.to(device=device, dtype=torch.long) | |
| ) | |
| n_mod_bin = payload.get("n_tokens_mod_bin") | |
| stats.n_tokens_mod_bin = ( | |
| torch.zeros(k, 3, device=device, dtype=torch.long) | |
| if n_mod_bin is None | |
| else n_mod_bin.to(device=device, dtype=torch.long) | |
| ) | |
| sens_sum = payload.get("sensitivity_sum") | |
| stats.sensitivity_sum = ( | |
| torch.zeros(k, device=device, dtype=torch.float32) | |
| if sens_sum is None | |
| else sens_sum.to(device=device, dtype=torch.float32) | |
| ) | |
| sens_n = payload.get("sensitivity_count") | |
| stats.sensitivity_count = ( | |
| torch.zeros(k, device=device, dtype=torch.float32) | |
| if sens_n is None | |
| else sens_n.to(device=device, dtype=torch.float32) | |
| ) | |
| rel = payload.get("rel_abs_z") | |
| stats.rel_abs_z = ( | |
| torch.zeros(d_int, device=device, dtype=torch.float32) | |
| if rel is None | |
| else rel.to(device=device, dtype=torch.float32) | |
| ) | |
| stats.token_cache_per_bin = int(payload.get("token_cache_per_bin") or 0) | |
| cache_z = payload.get("cache_z") | |
| if cache_z is None: | |
| stats.cache_z = None | |
| stats.cache_y = None | |
| stats.cache_gate = None | |
| stats.cache_mod = None | |
| else: | |
| stats.cache_z = [None if t is None else t.float().cpu() for t in cache_z] | |
| cache_y = payload.get("cache_y") or [None] * len(cache_z) | |
| cache_g = payload.get("cache_gate") or [None] * len(cache_z) | |
| cache_m = payload.get("cache_mod") or [None] * len(cache_z) | |
| stats.cache_y = [None if t is None else t.float().cpu() for t in cache_y] | |
| stats.cache_gate = [None if t is None else t.float().cpu() for t in cache_g] | |
| stats.cache_mod = [None if t is None else t.to(torch.int8).cpu() for t in cache_m] | |
| return stats | |
| def stacked_token_cache( | |
| stats: ExpertStats, | |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None: | |
| """Concatenate per-bin token reservoirs. Returns (z, y, gate) or None.""" | |
| if not stats.cache_z: | |
| return None | |
| zs, ys, gs = [], [], [] | |
| for z, y, g in zip(stats.cache_z, stats.cache_y or [], stats.cache_gate or []): | |
| if z is None or int(z.numel()) == 0: | |
| continue | |
| zs.append(z.float()) | |
| ys.append(y.float()) | |
| gs.append(g.float().reshape(-1)) | |
| if not zs: | |
| return None | |
| return torch.cat(zs, dim=0), torch.cat(ys, dim=0), torch.cat(gs, dim=0) | |
| def stacked_token_cache_mod( | |
| stats: ExpertStats, | |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor] | None: | |
| """Like stacked_token_cache but also returns the per-token modality id (-1 if unknown).""" | |
| packed = stacked_token_cache(stats) | |
| if packed is None: | |
| return None | |
| z, y, g = packed | |
| mods = [] | |
| for i, zb in enumerate(stats.cache_z): | |
| if zb is None or int(zb.numel()) == 0: | |
| continue | |
| m = None if stats.cache_mod is None or i >= len(stats.cache_mod) else stats.cache_mod[i] | |
| if m is None or int(m.numel()) != int(zb.shape[0]): | |
| m = torch.full((int(zb.shape[0]),), -1, dtype=torch.int8) | |
| mods.append(m.to(torch.int8)) | |
| return z, y, g, torch.cat(mods, dim=0) | |
| def perturbation_sensitivity( | |
| z: torch.Tensor, | |
| y: torch.Tensor, | |
| W_down: torch.Tensor, | |
| ratio: float, | |
| repeats: int, | |
| generator: torch.Generator | None, | |
| ) -> float: | |
| """Relative L2 of randomly masking ρ mid-neurons, averaged over repeats.""" | |
| n, d_int = z.shape | |
| del n | |
| n_mask = max(1, int(round(float(ratio) * d_int))) | |
| n_mask = min(n_mask, d_int) | |
| w = W_down.detach().to(device=z.device, dtype=torch.float32) | |
| z_f = z.float() | |
| y_norm = y.float().norm(dim=-1).clamp_min(PROJ_EPS) | |
| acc = 0.0 | |
| r = max(int(repeats), 1) | |
| for _ in range(r): | |
| idx = torch.randperm(d_int, generator=generator)[:n_mask].to(device=z.device) | |
| delta = z_f.index_select(1, idx) @ w.index_select(1, idx).T | |
| acc += float((delta.norm(dim=-1) / y_norm).mean()) | |
| return acc / r | |
| _perturbation_sensitivity = perturbation_sensitivity | |
| class LayerCollector: | |
| d_int: int | |
| num_experts: int | |
| device: torch.device | |
| mlp: object | |
| stats: dict[int, ExpertStats] = field(default_factory=dict) | |
| num_bins: int = DEFAULT_NUM_BINS | |
| collect_sensitivity: bool = True | |
| perturb_ratio: float = DEFAULT_PERTURB_RATIO | |
| perturb_repeats: int = DEFAULT_PERTURB_REPEATS | |
| collect_gram: bool = True | |
| token_cache_per_bin: int = DEFAULT_TOKEN_CACHE_PER_BIN | |
| current_timestep: float | None = None | |
| current_step: int | None = None | |
| num_steps: int | None = None | |
| def __post_init__(self): | |
| if self.mlp is None: | |
| raise ValueError("TENP collector needs mlp to read down_proj.weight") | |
| self.num_bins = max(int(self.num_bins), 1) | |
| self.token_cache_per_bin = max(int(self.token_cache_per_bin), 0) | |
| self.stats = { | |
| eid: ExpertStats.zeros( | |
| self.d_int, | |
| self.device, | |
| num_bins=self.num_bins, | |
| collect_gram=bool(self.collect_gram), | |
| token_cache_per_bin=self.token_cache_per_bin, | |
| ) | |
| for eid in range(self.num_experts) | |
| } | |
| self._sens_gen = torch.Generator(device="cpu") | |
| self._sens_gen.manual_seed(0) | |
| def _down_weight(self, expert_id: int) -> torch.Tensor: | |
| return self.mlp.experts[int(expert_id)].down_proj.weight | |
| def current_bin(self) -> int: | |
| mlp = self.mlp | |
| t = getattr(mlp, "diffusion_timestep", None) | |
| if t is None: | |
| t = self.current_timestep | |
| step_index = getattr(mlp, "diffusion_step_index", None) | |
| if step_index is None: | |
| step_index = self.current_step | |
| num_steps = getattr(mlp, "diffusion_num_steps", None) | |
| if num_steps is None: | |
| num_steps = self.num_steps | |
| return timestep_to_bin( | |
| t, | |
| self.num_bins, | |
| step_index=step_index, | |
| num_steps=num_steps, | |
| ) | |
| def __call__( | |
| self, | |
| expert_id: int, | |
| modality: torch.Tensor, | |
| gate: torch.Tensor, | |
| z: torch.Tensor, | |
| y: torch.Tensor, | |
| ) -> None: | |
| self.stats[int(expert_id)].update( | |
| z, | |
| y, | |
| gate, | |
| modality, | |
| W_down=self._down_weight(expert_id), | |
| bin_id=self.current_bin(), | |
| collect_sensitivity=self.collect_sensitivity, | |
| perturb_ratio=self.perturb_ratio, | |
| perturb_repeats=self.perturb_repeats, | |
| generator=self._sens_gen, | |
| ) | |
| def state_dict(self) -> dict: | |
| return { | |
| "d_int": int(self.d_int), | |
| "num_experts": int(self.num_experts), | |
| "num_bins": int(self.num_bins), | |
| "stats": {str(eid): st.state_dict() for eid, st in self.stats.items()}, | |
| } | |
| def from_state(cls, payload: dict, mlp, device="cpu") -> "LayerCollector": | |
| col = cls( | |
| d_int=int(payload["d_int"]), | |
| num_experts=int(payload["num_experts"]), | |
| device=torch.device(device), | |
| mlp=mlp, | |
| num_bins=int(payload.get("num_bins") or DEFAULT_NUM_BINS), | |
| collect_sensitivity=False, | |
| collect_gram=False, | |
| token_cache_per_bin=0, | |
| ) | |
| for eid_s, st in payload["stats"].items(): | |
| col.stats[int(eid_s)] = ExpertStats.from_state(st, device=device) | |
| if col.stats: | |
| col.num_bins = next(iter(col.stats.values())).num_bins | |
| return col | |