Make sLSTM checkpoints portable across backends (#127) - #128
Conversation
The sLSTM cell stores its recurrent kernel and bias in a backend-specific internal layout (sLSTMCell_vanilla vs sLSTMCell_cuda use different shapes and orderings). state_dict serialized those internal parameters directly, so a checkpoint saved with one backend could not be loaded into a cell using the other: the recurrent kernel raised a size mismatch and the bias loaded silently with a scrambled gate/head ordering -- even though the cell already exposes a single canonical (external) layout via its ext2int/int2ext conversions and documents cross-backend conversion as an intended workflow. Register a state_dict hook that stores the recurrent kernel and bias in the backend-agnostic external layout, and a load-state-dict pre-hook that converts an external-layout checkpoint back into the current backend's internal layout. External tensors are identified by rank (recurrent kernel 4D, bias 3D), so legacy checkpoints saved in the internal layout still load into a cell of their original backend. Adds regression tests covering cross-backend load (both directions), same-backend round-trips, and legacy internal-layout checkpoints.
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93efad0246
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| state_dict[recurrent_kernel_key] = module._recurrent_kernel_int2ext( | ||
| state_dict[recurrent_kernel_key] | ||
| ) | ||
| if bias_key in state_dict: | ||
| state_dict[bias_key] = module._bias_int2ext(state_dict[bias_key]) |
There was a problem hiding this comment.
Materialize canonical tensors before returning the state dict
When a vanilla-backed cell is exported through a serializer that requires contiguous tensors, such as safetensors.torch.save_file(model.state_dict(), ...), this hook now causes saving to fail. Both vanilla int2ext methods return non-contiguous views produced by permute, and the hook places those views directly in the state dict; typical multi-head configurations therefore trigger safetensors' non-contiguous-tensor rejection. Materialize the converted values with contiguous() before storing them.
Useful? React with 👍 / 👎.
The vanilla int2ext conversions return non-contiguous views (via permute), so the exported state_dict held non-contiguous tensors. Serializers that require contiguous tensors -- e.g. safetensors.torch.save_file -- reject those for typical multi-head configs. Call .contiguous() on the converted values in the state_dict hook. Adds a test asserting the exported tensors are contiguous.
|
Good catch, thanks. The vanilla Fixed in a7e54a9 by calling |
What
Fixes #127. A checkpoint saved with one sLSTM backend cannot be loaded into a cell using another backend:
Why
sLSTMCell_vanillaandsLSTMCell_cudakeep the recurrent kernel and bias in different internal layouts:(H, G·head_dim, head_dim)(H, head_dim, G·head_dim)(H, head_dim, G, head_dim)(H·G·head_dim,)gate-major(H·G·head_dim,)head-major(H, G, head_dim)state_dictserializes the internal_recurrent_kernel_/_bias_parameters directly. Loading across backends therefore fails: the recurrent kernel raises a size mismatch, and — more insidiously — the bias has the same flat shape in both backends, so it loads without error but with a scrambled gate/head ordering (silent weight corruption).The cell already defines a single canonical (external) layout via its
_..._int2ext/_..._ext2intconversions, exposes it through therecurrent_kernel/biasproxies, andsLSTMCell_cuda.__init__documents cross-backend conversion (skip_backend_init) as an intended workflow — so the checkpoint simply needs to be stored in that canonical layout.How
Two hooks registered in
sLSTMCellBase.__init__:_register_state_dict_hook— on save, convert_recurrent_kernel_/_bias_to the canonical external layout (int2ext)._register_load_state_dict_pre_hook— on load, convert an external-layout checkpoint back into the current backend's internal layout (ext2int).External tensors are identified by rank (recurrent kernel 4D, bias 3D). Legacy checkpoints saved in the old internal layout (recurrent kernel 3D, bias 1D) are left untouched, so they still load into a cell of their original backend — the change is backward compatible.
Verification (CPU, via
skip_backend_init=True)cuda → vanillaandvanilla → cudanow load with no error, and the canonical weights match exactly (allclose) — including the bias, which was previously corrupted silently.Tests
tests/test_slstm_backend_state_dict.pycovers all four cases above. The two cross-backend tests fail onmain(RuntimeError/ corrupted bias) and pass with the fix; same-backend and legacy tests pass both ways. They run on CPU.Disclosure: this change was prepared with AI assistance and reviewed/verified by me before submission.