Download src/diffusion/sampling.py from AnonMLuser/remdm-planner-minihack: direct link, hf CLI and curl.
- Browser
- Download file 20.9 kB
-
https://huggingface.co/AnonMLuser/remdm-planner-minihack/resolve/main/src/diffusion/sampling.py
- Command line
-
hf download hf://AnonMLuser/remdm-planner-minihack/src/diffusion/sampling.py
-
curl -L -o sampling.py https://huggingface.co/AnonMLuser/remdm-planner-minihack/resolve/main/src/diffusion/sampling.py
20.9 kB
| """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 | |
| # NLE hazard glyph IDs and char codes (walls, locked doors, lava, water) | |
| _HAZARD_GLYPHS: frozenset[int] = frozenset({2359, 2360, 2389, 2390}) | |
| _HAZARD_CHARS: frozenset[int] = frozenset( | |
| {ord("|"), ord("-"), ord("+"), ord("L"), ord("W")} | |
| ) | |
| # Cardinal action → (dy, dx) offsets | |
| _CARDINAL_OFFSETS: dict[int, tuple[int, int]] = { | |
| 0: (-1, 0), | |
| 1: (0, 1), | |
| 2: (1, 0), | |
| 3: (0, -1), | |
| } | |
| _N_PHYSICS_CHECK = 8 # only inspect the first N plan positions | |
| # Stability guards; values must match the craftax twin exactly. | |
| _SIGMA_DENOM_EPS = 1e-8 # sigma_max and posterior denominators | |
| # Demotion value for hazardous actions under the conf strategy. | |
| _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 # exclusive cumsum | |
| 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" | |
| # softmax(-psi) over committed positions, zero elsewhere, | |
| # scaled by eta * sigma_max: mirrors craftax ``sigma_conf``. | |
| 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}") | |
| 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) | |
| # Pre-compute numpy local crops for physics checks (CPU, batch loop) | |
| local_np: np.ndarray | None = None # [B, crop, crop] | |
| if physics_aware: | |
| local_np = local_obs.cpu().numpy() | |
| # Analytics buffers (only populated when return_analytics=True) | |
| path_per_step: list[np.ndarray] = [] | |
| tracking_confidence: list[float] = [] | |
| tracking_masked_count: list[int] = [] | |
| # Start fully masked; psi stores the decoding probability at the step | |
| # each token was last unmasked (+inf while masked), per ReMDM Sec 4.1. | |
| seq = torch.full((B, seq_len), mask_token, dtype=torch.long, device=device) | |
| psi = torch.full((B, seq_len), float("inf"), device=device) | |
| # Historical prefix (Diffuser Sec. 3.3; spec-method §6.1/§6.2, SHARED): | |
| # positions 0..hist_len-1 are observed, so they are fixed for the whole | |
| # of denoising - never unmasked away, never remasked. The craftax twin | |
| # is sample_plan_inpainting. | |
| 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) # [1, L] | |
| lock_mask = pos < hist_len.to(device).unsqueeze(1) # [B, L] | |
| seq = torch.where(lock_mask, history, seq) | |
| # ReMDM Algorithm 1 (Wang et al.). Masked tokens unmask via | |
| # independent Bernoulli draws from the approximate posterior; | |
| # committed tokens remask w.p. sigma from the Sec 4.1 schedule. | |
| # The craftax twin is sample_plan. | |
| 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))) | |
| # Discrete conditioning bin for the learned timestep embedding | |
| # (free per MDLM Sec 3.5: time conditioning is optional). | |
| 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"] # [B, seq_len, vocab] | |
| # Mask invalid action tokens (indices >= action_dim) | |
| logits[:, :, action_dim:] = float("-inf") | |
| # psi, the confidence the `conf` remask strategy orders by, is the | |
| # model's own probability for the token it commits — read off the raw | |
| # posterior, before temperature and nucleus filtering, matching the | |
| # craftax twin (`sampling.py:185-186`). Taking it after filtering | |
| # makes psi a property of the decoding settings rather than of the | |
| # model: whenever the nucleus collapses to a single token psi is | |
| # exactly 1.0 however uncertain the model really is, so those | |
| # positions are never remasked. The sampling distribution below is | |
| # untouched. | |
| raw_probs = F.softmax(logits, dim=-1) # [B, seq_len, vocab] | |
| logits = logits / cfg.temperature | |
| # Nucleus filtering | |
| logits = top_p_filter(logits, cfg.top_p) | |
| probs = F.softmax(logits, dim=-1) # [B, seq_len, action_dim] | |
| preds = Categorical(probs=probs).sample() # [B, seq_len] | |
| decode_prob = raw_probs.gather(-1, preds.unsqueeze(-1)).squeeze(-1) # [B, L] | |
| # Physics softener (unsourced engineering, default off): | |
| # demote hazardous cardinal actions to decode_prob=0.001 so the | |
| # conf strategy preferentially remasks them. | |
| if physics_aware and local_np is not None: | |
| preds_np = preds.cpu().numpy() # [B, seq_len] | |
| prob_override = decode_prob.clone() | |
| for b in range(B): | |
| crop_b = np.asarray(local_np[b]) # [crop, crop] | |
| 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 # [B, seq_len] | |
| # Remasking probability sigma in [0, sigma_max] (ReMDM eq 7) | |
| 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) | |
| # Algorithm 1 posterior: masked tokens unmask w.p. | |
| # (alpha_s - (1 - sigma) alpha_t) / (1 - alpha_t) | |
| 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) | |
| # Analytics tracking | |
| 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())) | |
| # Final greedy cleanup for any remaining masks (as in the craftax | |
| # twin); replaces the previous commit-all step and assertion. | |
| 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 | |
| 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, | |
| ) | |
| # Observed prefix stays fixed for the whole of decoding, as in | |
| # remdm_sample (Diffuser Sec. 3.3; spec-method §6.1/§6.2). | |
| 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"] # [B, seq_len, vocab] | |
| # Mask invalid action tokens | |
| logits[:, :, action_dim:] = float("-inf") | |
| # Greedy: argmax over softmax (no temperature, no top-K) | |
| probs = F.softmax(logits, dim=-1) # [B, seq_len, action_dim] | |
| confidences, preds = probs.max(dim=-1) # [B, seq_len] each | |
| # MaskGIT progressive unmasking by confidence | |
| num_to_unmask = max(1, int(seq_len * ratio)) | |
| is_masked = seq == mask_token # [B, seq_len] | |
| # Score only masked positions for unmasking | |
| 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) | |
| # No remasking in greedy mode | |
| # Force-commit any remaining masked tokens | |
| 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 | |
| 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) | |