Modilify-Mk1-MLX / modilify_mlx /latent_deliberation.py
ydy9038074's picture
Publish Modilify Mk1 MLX runtime
a066584 verified
Raw History Blame Contribute Delete
16.9 kB
# Copyright 2026 Modilify
# SPDX-License-Identifier: LicenseRef-Modilify-Open-Model-1.0
"""Fixed-shape latent deliberation state for Modilify Mk1 decoding.
PyTorch → MLX:
nn.LayerNorm mlx.nn.LayerNorm
nn.Linear mlx.nn.Linear
nn.MultiheadAttention in_proj BiasedMHA query/key/value_proj (split QKV)
nn.Sequential token_ff/memory_ff mlx.nn.Sequential (layers.0 / layers.2)
"""
from __future__ import annotations
from dataclasses import dataclass
import math
import mlx.core as mx
import mlx.nn as nn
@dataclass
class LatentDeliberationState:
"""Persistent, fixed-size state for one or more canvas episodes."""
token_latents: mx.array
memory_slots: mx.array
confidence: mx.array
entropy: mx.array
age: mx.array
token_changed: mx.array
confidence_delta: mx.array
entropy_delta: mx.array
ponder_steps: mx.array
stagnation_steps: mx.array
@classmethod
def empty(
cls,
*,
batch_size: int,
canvas_length: int,
latent_dim: int,
memory_slots: int,
dtype: mx.Dtype,
) -> "LatentDeliberationState":
"""Create a zero-initialized recurrent state."""
return cls(
token_latents=mx.zeros((batch_size, canvas_length, latent_dim), dtype=dtype),
memory_slots=mx.zeros((batch_size, memory_slots, latent_dim), dtype=dtype),
confidence=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
entropy=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
age=mx.zeros((batch_size, canvas_length), dtype=mx.int32),
token_changed=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
confidence_delta=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
entropy_delta=mx.zeros((batch_size, canvas_length), dtype=mx.float32),
ponder_steps=mx.zeros((batch_size,), dtype=mx.int32),
stagnation_steps=mx.zeros((batch_size,), dtype=mx.int32),
)
def shift(
self, committed: int, *, entropy_fill_value: float = 0.0
) -> "LatentDeliberationState":
"""Drop committed canvas positions without changing long-term memory."""
canvas_length = self.token_latents.shape[1]
if not 0 <= committed <= canvas_length:
raise ValueError("`committed` must be in [0, canvas_length].")
if committed == 0:
return self
def shifted(tensor: mx.array, fill_value: float | int = 0) -> mx.array:
result = mx.full(tensor.shape, fill_value, dtype=tensor.dtype)
if committed < canvas_length:
kept = tensor[:, committed:]
result = mx.concatenate(
[kept, mx.full(
(tensor.shape[0], committed, *tensor.shape[2:]),
fill_value,
dtype=tensor.dtype,
)],
axis=1,
) if tensor.ndim > 2 else mx.concatenate(
[
kept,
mx.full(
(tensor.shape[0], committed),
fill_value,
dtype=tensor.dtype,
),
],
axis=1,
)
return result
return LatentDeliberationState(
token_latents=shifted(self.token_latents),
memory_slots=self.memory_slots,
confidence=shifted(self.confidence),
entropy=shifted(self.entropy, entropy_fill_value),
age=shifted(self.age),
token_changed=shifted(self.token_changed),
confidence_delta=shifted(self.confidence_delta),
entropy_delta=shifted(self.entropy_delta),
ponder_steps=mx.zeros_like(self.ponder_steps),
stagnation_steps=mx.zeros_like(self.stagnation_steps),
)
def advance_trajectory_clocks(
ponder_steps: mx.array,
stagnation_steps: mx.array,
*,
commit_lengths: mx.array,
active_rows: mx.array,
progress_scores: mx.array,
min_progress: float,
) -> tuple[mx.array, mx.array]:
"""Advance useful-ponder and true-stagnation clocks for each row."""
if min_progress < 0:
raise ValueError("`min_progress` must be non-negative.")
if not (
ponder_steps.shape
== stagnation_steps.shape
== commit_lengths.shape
== active_rows.shape
== progress_scores.shape
):
raise ValueError("Trajectory clock inputs must share shape [batch].")
committed = commit_lengths > 0
waiting = active_rows & (~committed)
improving = progress_scores >= min_progress
next_ponder = mx.where(
committed,
mx.zeros_like(ponder_steps),
ponder_steps + waiting.astype(mx.int32),
)
next_stagnation = mx.where(
committed,
mx.zeros_like(stagnation_steps),
mx.where(
waiting & improving,
mx.zeros_like(stagnation_steps),
stagnation_steps + waiting.astype(mx.int32),
),
)
return next_ponder.astype(mx.int32), next_stagnation.astype(mx.int32)
def should_force_trajectory_jump(
ponder_steps: mx.array,
stagnation_steps: mx.array,
*,
max_ponder_steps: int,
stagnation_threshold: int,
) -> mx.array:
"""Return whether a row has exhausted ponder or stagnation budget."""
if max_ponder_steps <= 0 or stagnation_threshold <= 0:
raise ValueError("Trajectory jump limits must be positive.")
return (ponder_steps >= max_ponder_steps) | (
stagnation_steps >= stagnation_threshold
)
class BiasedMHA(nn.Module):
"""Explicit QKV attention matching PyTorch MultiheadAttention weights."""
def __init__(self, dims: int, num_heads: int) -> None:
super().__init__()
if dims % num_heads:
raise ValueError("`dims` must be divisible by `num_heads`.")
self.num_heads = num_heads
self.head_dim = dims // num_heads
self.scale = self.head_dim**-0.5
self.query_proj = nn.Linear(dims, dims, bias=True)
self.key_proj = nn.Linear(dims, dims, bias=True)
self.value_proj = nn.Linear(dims, dims, bias=True)
self.out_proj = nn.Linear(dims, dims, bias=True)
def __call__(
self,
queries: mx.array,
keys: mx.array,
values: mx.array,
mask: mx.array | None = None,
) -> mx.array:
batch, query_len, dims = queries.shape
key_len = keys.shape[1]
queries = self.query_proj(queries).reshape(
batch, query_len, self.num_heads, self.head_dim
).transpose(0, 2, 1, 3)
keys = self.key_proj(keys).reshape(
batch, key_len, self.num_heads, self.head_dim
).transpose(0, 2, 1, 3)
values = self.value_proj(values).reshape(
batch, key_len, self.num_heads, self.head_dim
).transpose(0, 2, 1, 3)
attn_mask = None
if mask is not None:
attn_mask = mask.reshape(1, 1, mask.shape[-2], mask.shape[-1])
output = mx.fast.scaled_dot_product_attention(
queries, keys, values, scale=self.scale, mask=attn_mask
)
output = output.transpose(0, 2, 1, 3).reshape(batch, query_len, dims)
return self.out_proj(output)
def _silu(x: mx.array) -> mx.array:
return x * mx.sigmoid(x)
class _TemporalTransformerCell(nn.Module):
"""One-step recurrent token update with fixed-slot memory attention."""
def __init__(
self,
latent_dim: int,
num_heads: int,
local_attention_window: int,
) -> None:
super().__init__()
self.state_norm = nn.LayerNorm(latent_dim)
self.observation_norm = nn.LayerNorm(latent_dim)
self.memory_address_norm = nn.LayerNorm(latent_dim)
self.memory_value_norm = nn.LayerNorm(latent_dim)
self.temporal_update = nn.Linear(2 * latent_dim, 2 * latent_dim)
self.local_attention = BiasedMHA(latent_dim, num_heads)
self.local_attention_window = local_attention_window
self.token_memory_attention = BiasedMHA(latent_dim, num_heads)
self.memory_token_attention = BiasedMHA(latent_dim, num_heads)
self.token_ff_norm = nn.LayerNorm(latent_dim)
self.memory_ff_norm = nn.LayerNorm(latent_dim)
self.stored_token_norm = nn.LayerNorm(latent_dim)
self.stored_memory_norm = nn.LayerNorm(latent_dim)
expansion = latent_dim * 4
self.token_ff = nn.Sequential(
nn.Linear(latent_dim, expansion),
nn.SiLU(),
nn.Linear(expansion, latent_dim),
)
self.memory_ff = nn.Sequential(
nn.Linear(latent_dim, expansion),
nn.SiLU(),
nn.Linear(expansion, latent_dim),
)
def _local_mask(self, length: int, dtype: mx.Dtype) -> mx.array:
positions = mx.arange(length)
allowed = (
mx.abs(positions[:, None] - positions[None, :]) < self.local_attention_window
)
return mx.where(
allowed,
mx.zeros((length, length), dtype=dtype),
mx.full((length, length), mx.finfo(dtype).min, dtype=dtype),
)
def __call__(
self,
previous_tokens: mx.array,
observation: mx.array,
memory: mx.array,
memory_slot_identity: mx.array,
) -> tuple[mx.array, mx.array]:
gate_logits, candidate = mx.split(
self.temporal_update(
mx.concatenate(
(
self.state_norm(previous_tokens),
self.observation_norm(observation),
),
axis=-1,
)
),
2,
axis=-1,
)
gate = mx.sigmoid(gate_logits)
tokens = gate * previous_tokens + (1.0 - gate) * _silu(candidate)
local_mask = self._local_mask(tokens.shape[1], tokens.dtype)
normed_tokens = self.state_norm(tokens)
tokens = tokens + self.local_attention(
normed_tokens, normed_tokens, normed_tokens, mask=local_mask
)
addressed_memory = self.memory_address_norm(memory + memory_slot_identity)
memory_values = self.memory_value_norm(memory)
normed_tokens = self.state_norm(tokens)
tokens = tokens + self.token_memory_attention(
normed_tokens, addressed_memory, memory_values
)
tokens = tokens + self.token_ff(self.token_ff_norm(tokens))
normed_tokens = self.state_norm(tokens)
memory = memory + self.memory_token_attention(
addressed_memory, normed_tokens, normed_tokens
)
memory = memory + self.memory_ff(self.memory_ff_norm(memory))
return (
self.stored_token_norm(tokens),
self.stored_memory_norm(memory) + memory_slot_identity,
)
class LatentDeliberationTransformer(nn.Module):
"""Small recurrent Transformer that compresses repeated denoise context."""
def __init__(
self,
*,
hidden_size: int,
latent_dim: int = 512,
memory_slots: int = 16,
num_layers: int = 2,
num_heads: int = 8,
local_attention_window: int = 32,
dropout: float = 0.0,
) -> None:
super().__init__()
del dropout
if latent_dim % num_heads:
raise ValueError("`latent_dim` must be divisible by `num_heads`.")
if local_attention_window <= 0:
raise ValueError("`local_attention_window` must be positive.")
self.hidden_size = hidden_size
self.latent_dim = latent_dim
self.memory_slots = memory_slots
self.heavy_projection = nn.Linear(hidden_size, latent_dim, bias=False)
self.embedding_projection = nn.Linear(hidden_size, latent_dim, bias=False)
self.scalar_projection = nn.Linear(11, latent_dim, bias=False)
self.blocks = [
_TemporalTransformerCell(latent_dim, num_heads, local_attention_window)
for _ in range(num_layers)
]
self.output_norm = nn.LayerNorm(latent_dim)
self.output_projection = nn.Linear(latent_dim, hidden_size, bias=False)
self.memory_slot_identity = mx.zeros((memory_slots, latent_dim))
def scaled_memory_slot_identity(
self,
*,
batch_size: int,
dtype: mx.Dtype,
) -> mx.array:
"""Return unit directions scaled to LayerNorm RMS."""
identity = self.memory_slot_identity.astype(mx.float32)
identity = identity / mx.maximum(
mx.linalg.norm(identity, axis=-1, keepdims=True), 1.0e-12
)
identity = identity * math.sqrt(self.latent_dim)
return mx.broadcast_to(
identity.astype(dtype)[None, :, :],
(batch_size, self.memory_slots, self.latent_dim),
)
def project_context(self, token_latents: mx.array) -> mx.array:
"""Translate latent state into a self-conditioning embedding."""
return self.output_projection(self.output_norm(token_latents))
def __call__(
self,
*,
heavy_hidden: mx.array,
token_embeddings: mx.array,
confidence: mx.array,
entropy: mx.array,
state: LatentDeliberationState,
) -> tuple[mx.array, LatentDeliberationState]:
"""Advance latent memory and produce decoder self-conditioning."""
if heavy_hidden.ndim != 3:
raise ValueError("`heavy_hidden` must have shape [batch, canvas, hidden].")
if heavy_hidden.shape != token_embeddings.shape:
raise ValueError(
"`heavy_hidden` and `token_embeddings` must have the same shape."
)
batch_size, canvas_length, hidden_size = heavy_hidden.shape
if hidden_size != self.hidden_size:
raise ValueError("Unexpected hidden size for latent deliberation.")
expected_state = (batch_size, canvas_length, self.latent_dim)
if state.token_latents.shape != expected_state:
raise ValueError("State token latents do not match the current canvas.")
if state.memory_slots.shape != (
batch_size,
self.memory_slots,
self.latent_dim,
):
raise ValueError("State memory slots do not match this module.")
dtype = heavy_hidden.dtype
position = mx.linspace(-1.0, 1.0, canvas_length, dtype=dtype)
position = mx.broadcast_to(position[None, :], (batch_size, canvas_length))
age = mx.minimum(state.age.astype(dtype), 32767).astype(dtype)
entropy_delta = state.entropy_delta.astype(dtype)
scalars = mx.stack(
(
confidence.astype(dtype),
mx.log1p(entropy.astype(dtype)),
mx.log1p(age),
position,
state.token_changed.astype(dtype),
state.confidence_delta.astype(dtype),
mx.sign(entropy_delta) * mx.log1p(mx.abs(entropy_delta)),
mx.broadcast_to(
mx.log1p(state.ponder_steps.astype(dtype))[:, None],
(batch_size, canvas_length),
),
mx.broadcast_to(
mx.log1p(state.stagnation_steps.astype(dtype))[:, None],
(batch_size, canvas_length),
),
confidence.astype(dtype)
* mx.exp(-mx.maximum(entropy.astype(dtype), 0.0)),
mx.maximum(state.confidence_delta.astype(dtype), 0.0)
+ mx.log1p(mx.maximum(-entropy_delta, 0.0)),
),
axis=-1,
)
observation = (
self.heavy_projection(heavy_hidden)
+ self.embedding_projection(token_embeddings)
+ self.scalar_projection(scalars)
)
tokens = state.token_latents
memory = state.memory_slots
slot_identity = self.scaled_memory_slot_identity(
batch_size=batch_size,
dtype=memory.dtype,
)
for block in self.blocks:
tokens, memory = block(tokens, observation, memory, slot_identity)
observation = tokens
next_state = LatentDeliberationState(
token_latents=tokens,
memory_slots=memory,
confidence=confidence.astype(mx.float32),
entropy=entropy.astype(mx.float32),
age=state.age,
token_changed=state.token_changed,
confidence_delta=state.confidence_delta,
entropy_delta=state.entropy_delta,
ponder_steps=state.ponder_steps,
stagnation_steps=state.stagnation_steps,
)
return self.project_context(tokens), next_state