| """ReMDM reverse denoising with remasking strategies. |
| |
| remdm_sample implements ReMDM Algorithm 1 (Wang et al.): Bernoulli |
| posterior unmasking with the Section 4.1 remasking schedules. The |
| craftax twin is src/diffusion/sampling.py:sample_plan. greedy_sample |
| is a separate MaskGIT-style argmax decoder used only for DAgger |
| collection (a documented engineering choice). |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from types import SimpleNamespace |
|
|
| import numpy as np |
| import torch |
| import torch.nn.functional as F |
| from torch import Tensor |
| from torch.distributions import Categorical |
|
|
| from src.diffusion.schedules import get_schedule |
|
|
| |
| _HAZARD_GLYPHS: frozenset[int] = frozenset({2359, 2360, 2389, 2390}) |
| _HAZARD_CHARS: frozenset[int] = frozenset( |
| {ord("|"), ord("-"), ord("+"), ord("L"), ord("W")} |
| ) |
| |
| _CARDINAL_OFFSETS: dict[int, tuple[int, int]] = { |
| 0: (-1, 0), |
| 1: (0, 1), |
| 2: (1, 0), |
| 3: (0, -1), |
| } |
| _N_PHYSICS_CHECK = 8 |
|
|
| |
| _SIGMA_DENOM_EPS = 1e-8 |
| |
| _HAZARD_DECODE_PROB = 0.001 |
|
|
|
|
| def _check_hazard(local_crop: np.ndarray, action: int) -> bool: |
| """Return True if *action* from the agent's centre steps into a hazard. |
| |
| Args: |
| local_crop: ``[crop_size, crop_size]`` glyph array. |
| action: Cardinal action index (0=N, 1=E, 2=S, 3=W). |
| |
| Returns: |
| ``True`` when the target cell contains a hazard glyph. |
| """ |
| if action not in _CARDINAL_OFFSETS: |
| return False |
| cs = local_crop.shape[0] |
| cy, cx = cs // 2, cs // 2 |
| dy, dx = _CARDINAL_OFFSETS[action] |
| ny, nx = cy + dy, cx + dx |
| if not (0 <= ny < cs and 0 <= nx < cs): |
| return True |
| glyph = int(local_crop[ny, nx]) |
| return glyph in _HAZARD_GLYPHS or glyph in _HAZARD_CHARS |
|
|
|
|
| def top_p_filter(logits: Tensor, top_p: float) -> Tensor: |
| """Nucleus filtering (ReMDM Sec 5). |
| |
| Keeps the smallest prefix of the descending-sorted distribution whose |
| cumulative mass reaches ``top_p``; all other logits go to ``-inf``. |
| Mirrors the craftax twin ``_nucleus_sample`` cutoff semantics. |
| |
| Args: |
| logits: Raw logits. Shape ``[..., V]``. |
| top_p: Nucleus threshold in (0, 1]; ``>= 1`` disables filtering. |
| |
| Returns: |
| Filtered logits with out-of-nucleus entries set to ``-inf``. |
| """ |
| if top_p is None or top_p >= 1.0: |
| return logits |
| probs = F.softmax(logits, dim=-1) |
| sorted_p, sorted_idx = probs.sort(dim=-1, descending=True) |
| cutoff = sorted_p.cumsum(dim=-1) - sorted_p |
| remove_sorted = cutoff >= top_p |
| remove = remove_sorted.gather(-1, sorted_idx.argsort(dim=-1)) |
| return logits.masked_fill(remove, float("-inf")) |
|
|
|
|
| def _compute_remask_prob( |
| strategy: str, |
| eta: float, |
| sigma_max: float, |
| psi: Tensor | None, |
| committed: Tensor | None = None, |
| ) -> Tensor | float: |
| """Compute per-token remasking probability. |
| |
| The ``conf`` strategy consumes the stored decoding probability |
| ``psi`` from the step each token was last unmasked (ReMDM Sec 4.1), |
| not the current step's fresh confidence. |
| |
| Args: |
| strategy: One of ``"rescale"``, ``"cap"``, ``"conf"``. |
| eta: Base remasking strength hyperparameter. |
| sigma_max: ReMDM eq 7 value ``min(1, (1 - alpha_s) / alpha_t)``, |
| computed by the caller. |
| psi: Stored decoding probabilities at last unmask. Shape |
| ``[B, L]``, ``+inf`` at masked positions. Required only for |
| the ``"conf"`` strategy. |
| committed: Boolean mask of committed (non-masked) positions. |
| Required only for the ``"conf"`` strategy. |
| |
| Returns: |
| Scalar or ``[B, L]`` tensor of remasking probabilities. |
| """ |
| if strategy == "rescale": |
| return eta * sigma_max |
| if strategy == "cap": |
| return min(eta, sigma_max) |
| if strategy == "conf": |
| assert psi is not None, "conf strategy requires psi" |
| assert committed is not None, "conf strategy requires the committed mask" |
| |
| |
| neg = torch.where( |
| committed, |
| -psi, |
| torch.tensor(float("-inf"), device=psi.device, dtype=psi.dtype), |
| ) |
| any_committed = committed.any(dim=-1, keepdim=True) |
| safe = torch.where(any_committed, neg, torch.zeros_like(neg)) |
| weights = torch.softmax(safe, dim=-1) |
| return torch.where( |
| committed, weights * (eta * sigma_max), torch.zeros_like(weights) |
| ) |
| raise ValueError(f"Unknown remask strategy: {strategy}") |
|
|
|
|
| @torch.no_grad() |
| def remdm_sample( |
| model: torch.nn.Module, |
| local_obs: Tensor, |
| global_obs: Tensor, |
| cfg: SimpleNamespace, |
| device: torch.device | str, |
| physics_aware: bool = True, |
| blind_global: bool = False, |
| return_analytics: bool = False, |
| num_steps: int | None = None, |
| history: Tensor | None = None, |
| hist_len: Tensor | None = None, |
| ) -> Tensor | tuple[Tensor, list, list[float], list[int]]: |
| """Generate action sequences via iterative ReMDM denoising. |
| |
| Args: |
| model: Denoising model with forward signature |
| ``(local_obs, global_obs, action_seq, t_discrete) -> dict``. |
| local_obs: Local crop observations. Shape ``[B, 9, 9]``. |
| global_obs: Global map observations. Shape ``[B, 21, 79]``. |
| cfg: Config namespace with ``seq_len``, ``mask_token``, |
| ``action_dim``, ``diffusion_steps_eval``, ``temperature``, |
| ``top_p``, ``eta``, ``remask_strategy``, ``noise_schedule``. |
| device: Torch device. |
| physics_aware: If ``True``, soft-penalise hazardous cardinal actions |
| by overriding their stored decoding probability to ``0.001`` so |
| the ``conf`` strategy preferentially remasks them. Only checks |
| the first ``_N_PHYSICS_CHECK`` positions. |
| blind_global: If ``True``, zero out the global map observation |
| (local-only ablation). |
| return_analytics: If ``True``, also return per-step analytics as |
| ``(seq, path_per_step, tracking_confidence, tracking_masked)``. |
| num_steps: Override number of denoising steps (default uses |
| ``cfg.diffusion_steps_eval``). |
| history: ``[B, seq_len]`` already-executed actions to lock into |
| the plan's leading positions (planning as inpainting, |
| Diffuser Sec. 3.3). ``None`` plans from a fully masked |
| sequence. |
| hist_len: ``[B]`` number of leading positions to lock. Required |
| with ``history``. |
| |
| Returns: |
| When ``return_analytics=False`` (default): fully committed action |
| sequence of shape ``[B, seq_len]``, int64, with no MASK tokens. |
| |
| When ``return_analytics=True``: tuple |
| ``(seq, path_per_step, tracking_confidence, tracking_masked_count)`` |
| where ``path_per_step`` is a list of ``[seq_len]`` numpy arrays, |
| ``tracking_confidence`` a list of per-step avg unmasked confidence |
| floats, and ``tracking_masked_count`` a list of masked-token counts. |
| """ |
| B = local_obs.shape[0] |
| seq_len = cfg.seq_len |
| mask_token = cfg.mask_token |
| action_dim = cfg.action_dim |
| K = num_steps if num_steps is not None else cfg.diffusion_steps_eval |
| schedule_fn = get_schedule(cfg.noise_schedule) |
|
|
| local_obs = local_obs.to(device) |
| global_obs = global_obs.to(device) |
|
|
| if blind_global: |
| global_obs = torch.zeros_like(global_obs) |
|
|
| |
| local_np: np.ndarray | None = None |
| if physics_aware: |
| local_np = local_obs.cpu().numpy() |
|
|
| |
| path_per_step: list[np.ndarray] = [] |
| tracking_confidence: list[float] = [] |
| tracking_masked_count: list[int] = [] |
|
|
| |
| |
| seq = torch.full((B, seq_len), mask_token, dtype=torch.long, device=device) |
| psi = torch.full((B, seq_len), float("inf"), device=device) |
|
|
| |
| |
| |
| |
| lock_mask: Tensor | None = None |
| if history is not None: |
| if hist_len is None: |
| raise ValueError("remdm_sample: history requires hist_len") |
| history = history.to(device) |
| pos = torch.arange(seq_len, device=device).unsqueeze(0) |
| lock_mask = pos < hist_len.to(device).unsqueeze(1) |
| seq = torch.where(lock_mask, history, seq) |
|
|
| |
| |
| |
| |
| for idx in range(K): |
| t = (K - idx) / K |
| s = (K - idx - 1) / K |
| alpha_t = float(schedule_fn(torch.tensor(t))) |
| alpha_s = float(schedule_fn(torch.tensor(s))) |
| |
| |
| t_discrete = torch.full( |
| (B,), |
| min(int(t * cfg.num_diffusion_steps), cfg.num_diffusion_steps - 1), |
| dtype=torch.long, |
| device=device, |
| ) |
|
|
| out = model(local_obs, global_obs, seq, t_discrete) |
| logits = out["actions"] |
|
|
| |
| logits[:, :, action_dim:] = float("-inf") |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| raw_probs = F.softmax(logits, dim=-1) |
|
|
| logits = logits / cfg.temperature |
|
|
| |
| logits = top_p_filter(logits, cfg.top_p) |
|
|
| probs = F.softmax(logits, dim=-1) |
| preds = Categorical(probs=probs).sample() |
|
|
| decode_prob = raw_probs.gather(-1, preds.unsqueeze(-1)).squeeze(-1) |
|
|
| |
| |
| |
| if physics_aware and local_np is not None: |
| preds_np = preds.cpu().numpy() |
| prob_override = decode_prob.clone() |
| for b in range(B): |
| crop_b = np.asarray(local_np[b]) |
| for pos in range(min(_N_PHYSICS_CHECK, seq_len)): |
| action = int(preds_np[b, pos]) |
| if _check_hazard(crop_b, action): |
| prob_override[b, pos] = _HAZARD_DECODE_PROB |
| decode_prob = prob_override |
|
|
| committed = seq != mask_token |
|
|
| |
| sigma_max = min(1.0, (1.0 - alpha_s) / max(alpha_t, _SIGMA_DENOM_EPS)) |
| sigma = _compute_remask_prob( |
| cfg.remask_strategy, cfg.eta, sigma_max, psi, committed |
| ) |
| if not isinstance(sigma, Tensor): |
| sigma = torch.full((B, seq_len), float(sigma), device=device) |
| if lock_mask is not None: |
| sigma = torch.where(lock_mask, torch.zeros_like(sigma), sigma) |
|
|
| |
| |
| p_unmask = torch.clamp( |
| (alpha_s - (1.0 - sigma) * alpha_t) / max(1.0 - alpha_t, _SIGMA_DENOM_EPS), |
| 0.0, |
| 1.0, |
| ) |
|
|
| do_unmask = ~committed & (torch.rand(B, seq_len, device=device) < p_unmask) |
| do_remask = committed & (torch.rand(B, seq_len, device=device) < sigma) |
|
|
| seq = torch.where(do_unmask, preds, seq) |
| seq = torch.where(do_remask, mask_token, seq) |
| psi = torch.where(do_unmask, decode_prob, psi) |
| psi = torch.where(do_remask, torch.full_like(psi, float("inf")), psi) |
|
|
| if lock_mask is not None: |
| seq = torch.where(lock_mask, history, seq) |
| psi = torch.where(lock_mask, torch.full_like(psi, float("inf")), psi) |
|
|
| |
| if return_analytics: |
| path_per_step.append(seq[0].cpu().numpy().copy()) |
| still_masked = seq[0] == mask_token |
| unmasked_prob = psi[0][~still_masked] |
| avg_conf = unmasked_prob.mean().item() if unmasked_prob.numel() > 0 else 0.0 |
| tracking_confidence.append(avg_conf) |
| tracking_masked_count.append(int(still_masked.sum().item())) |
|
|
| |
| |
| still_masked = seq == mask_token |
| if still_masked.any(): |
| t_zero = torch.zeros(B, dtype=torch.long, device=device) |
| out = model(local_obs, global_obs, seq, t_zero) |
| logits = out["actions"] |
| logits[:, :, action_dim:] = float("-inf") |
| seq = torch.where(still_masked, logits.argmax(dim=-1), seq) |
| if lock_mask is not None: |
| seq = torch.where(lock_mask, history, seq) |
|
|
| if return_analytics: |
| return seq, path_per_step, tracking_confidence, tracking_masked_count |
| return seq |
|
|
|
|
| @torch.no_grad() |
| def greedy_sample( |
| model: torch.nn.Module, |
| local_obs: Tensor, |
| global_obs: Tensor, |
| cfg: SimpleNamespace, |
| device: torch.device | str, |
| blind_global: bool = False, |
| num_steps: int | None = None, |
| history: Tensor | None = None, |
| hist_len: Tensor | None = None, |
| ) -> Tensor: |
| """Greedy (argmax) MaskGIT sampling — no temperature, top-K, or remasking. |
| |
| Used by ``DataCollector`` during DAgger for deterministic rollouts, |
| matching the reference ``run_model_episode`` behaviour. |
| |
| Args: |
| model: Denoising model. |
| local_obs: Shape ``[B, 9, 9]``. |
| global_obs: Shape ``[B, 21, 79]``. |
| cfg: Config namespace. |
| device: Torch device. |
| blind_global: Zero out global map (local-only ablation). |
| history: ``[B, seq_len]`` already-executed actions to lock into |
| the plan's leading positions (see ``remdm_sample``). |
| hist_len: ``[B]`` number of leading positions to lock. |
| |
| Returns: |
| Fully committed action sequence ``[B, seq_len]``, int64. |
| """ |
| B = local_obs.shape[0] |
| seq_len = cfg.seq_len |
| mask_token = cfg.mask_token |
| action_dim = cfg.action_dim |
| K = num_steps if num_steps is not None else cfg.diffusion_steps_eval |
|
|
| local_obs = local_obs.to(device) |
| global_obs = global_obs.to(device) |
| if blind_global: |
| global_obs = torch.zeros_like(global_obs) |
|
|
| seq = torch.full( |
| (B, seq_len), |
| mask_token, |
| dtype=torch.long, |
| device=device, |
| ) |
|
|
| |
| |
| lock_mask: Tensor | None = None |
| if history is not None: |
| if hist_len is None: |
| raise ValueError("greedy_sample: history requires hist_len") |
| history = history.to(device) |
| pos = torch.arange(seq_len, device=device).unsqueeze(0) |
| lock_mask = pos < hist_len.to(device).unsqueeze(1) |
| seq = torch.where(lock_mask, history, seq) |
|
|
| for k in range(1, K + 1): |
| ratio = k / K |
| t_discrete = torch.full( |
| (B,), |
| int(cfg.num_diffusion_steps * (1.0 - ratio)), |
| dtype=torch.long, |
| device=device, |
| ) |
|
|
| out = model(local_obs, global_obs, seq, t_discrete) |
| logits = out["actions"] |
|
|
| |
| logits[:, :, action_dim:] = float("-inf") |
|
|
| |
| probs = F.softmax(logits, dim=-1) |
| confidences, preds = probs.max(dim=-1) |
|
|
| |
| num_to_unmask = max(1, int(seq_len * ratio)) |
| is_masked = seq == mask_token |
|
|
| |
| scores = confidences.clone() |
| scores[~is_masked] = -1.0 |
| _, topk_idx = scores.topk(num_to_unmask, dim=-1) |
|
|
| unmask_mask = torch.zeros_like(seq, dtype=torch.bool) |
| unmask_mask.scatter_(1, topk_idx, True) |
| unmask_mask = unmask_mask & is_masked |
|
|
| seq = torch.where(unmask_mask, preds, seq) |
| if lock_mask is not None: |
| seq = torch.where(lock_mask, history, seq) |
|
|
| |
|
|
| |
| still_masked = seq == mask_token |
| if still_masked.any(): |
| t_zero = torch.zeros(B, dtype=torch.long, device=device) |
| out = model(local_obs, global_obs, seq, t_zero) |
| logits = out["actions"] |
| logits[:, :, action_dim:] = float("-inf") |
| preds = logits.argmax(dim=-1) |
| seq = torch.where(still_masked, preds, seq) |
| if lock_mask is not None: |
| seq = torch.where(lock_mask, history, seq) |
|
|
| return seq |
|
|
| @dataclass |
| class LockedPrefix: |
| """Executed-action prefix for inpainted receding-horizon replanning. |
| |
| A replan is conditioned on what the agent has actually done since |
| the current plan window opened: those positions are observed, so |
| they are locked for the whole of denoising rather than re-generated |
| (Diffuser Sec. 3.3; spec-method §6.1/§6.2, SHARED - the craftax |
| twin's ``sample_plan_inpainting`` + ``mpc_step`` do the same). |
| Author decision 2026-08-16. |
| |
| A window closes once ``seq_len`` actions have been executed from |
| it; the next replan starts a fresh, fully masked window. |
| |
| Args: |
| n: Number of parallel episodes. |
| seq_len: Plan length. |
| mask_token: MASK token id, used to fill unwritten positions. |
| """ |
|
|
| n: int |
| seq_len: int |
| mask_token: int |
|
|
| def __post_init__(self) -> None: |
| self.history = np.full( |
| (self.n, self.seq_len), self.mask_token, dtype=np.int64 |
| ) |
| self.hist_len = np.zeros(self.n, dtype=np.int64) |
|
|
| def start_window(self, idx: np.ndarray | None = None) -> None: |
| """Open a fresh window for any row whose plan is used up. |
| |
| Args: |
| idx: Rows about to replan; ``None`` means all rows. |
| """ |
| rows = np.arange(self.n) if idx is None else np.asarray(idx) |
| full = rows[self.hist_len[rows] >= self.seq_len] |
| if full.size: |
| self.history[full] = self.mask_token |
| self.hist_len[full] = 0 |
|
|
| def as_tensors( |
| self, idx: np.ndarray, device: torch.device | str |
| ) -> tuple[Tensor, Tensor]: |
| """Return ``(history, hist_len)`` for ``idx`` as device tensors. |
| |
| Args: |
| idx: Rows being replanned. |
| device: Torch device. |
| """ |
| return ( |
| torch.from_numpy(self.history[idx]).to(device), |
| torch.from_numpy(self.hist_len[idx]).to(device), |
| ) |
|
|
| def record(self, i: int, action: int) -> None: |
| """Append an executed action to row ``i``'s prefix. |
| |
| Args: |
| i: Row index. |
| action: Action just executed. |
| """ |
| self.history[i, self.hist_len[i]] = action |
| self.hist_len[i] += 1 |
|
|
| def reset(self, i: int) -> None: |
| """Clear row ``i`` (episode boundary). |
| |
| Args: |
| i: Row index. |
| """ |
| self.history[i] = self.mask_token |
| self.hist_len[i] = 0 |
|
|
| def is_full(self, i: int) -> bool: |
| """Whether row ``i``'s window has no unexecuted positions left. |
| |
| Args: |
| i: Row index. |
| """ |
| return bool(self.hist_len[i] >= self.seq_len) |
|
|