Summary
_GDNStateCapture.rollback in dflash/model_mlx.py:338-355 indexes self._gdn_inputs[j] and self.conv_data[j] with j advancing only for not c.is_trimmable() cache entries:
def rollback(self, cache, accepted, trim):
j = 0
for c in cache:
if c.is_trimmable():
c.trim(trim)
else:
q, k, v, a, b, A_log, dt_bias, init_state, mask = self._gdn_inputs[j]
...
conv_input, K = self.conv_data[j]
c.cache[0] = conv_input[:, accepted + 1 : accepted + K]
j += 1
This silently relies on the invariant "every non-trimmable cache entry corresponds to exactly one GatedDeltaNet layer". The capture lists are populated only by the monkey-patched GatedDeltaNet.__call__, so if mlx_lm ever introduces another non-trimmable cache type (a different SSM/recurrent layer, a custom DDT cache, etc.), j would still advance for it while _gdn_inputs[j] holds data from a different layer — silently producing rollback corruption rather than an exception.
Why it matters
Today this is true for the Qwen3.5 family on mlx_lm 0.31.x, but the invariant is undocumented and unguarded:
Suggested fix
Add a single assertion at the top of rollback:
def rollback(self, cache, accepted, trim):
n_non_trimmable = sum(1 for c in cache if not c.is_trimmable())
assert n_non_trimmable == len(self._gdn_inputs), (
f"non-trimmable cache count ({n_non_trimmable}) != "
f"captured GDN inputs ({len(self._gdn_inputs)}); "
"DFlash MLX rollback assumes every non-trimmable cache is a GatedDeltaNet"
)
j = 0
for c in cache:
...
Cheap to add; converts a silent corruption into a loud failure that future mlx-lm bumps catch immediately.
Happy to send a PR if the team agrees.
Environment
Summary
_GDNStateCapture.rollbackindflash/model_mlx.py:338-355indexesself._gdn_inputs[j]andself.conv_data[j]withjadvancing only fornot c.is_trimmable()cache entries:This silently relies on the invariant "every non-trimmable cache entry corresponds to exactly one
GatedDeltaNetlayer". The capture lists are populated only by the monkey-patchedGatedDeltaNet.__call__, so ifmlx_lmever introduces another non-trimmable cache type (a different SSM/recurrent layer, a custom DDT cache, etc.),jwould still advance for it while_gdn_inputs[j]holds data from a different layer — silently producing rollback corruption rather than an exception.Why it matters
Today this is true for the Qwen3.5 family on
mlx_lm 0.31.x, but the invariant is undocumented and unguarded:Suggested fix
Add a single assertion at the top of
rollback:Cheap to add; converts a silent corruption into a loud failure that future mlx-lm bumps catch immediately.
Happy to send a PR if the team agrees.
Environment
HEAD6e0c951pyproject.toml'smlx-lm>=0.31.2)