-
Notifications
You must be signed in to change notification settings - Fork 250
feat: direct DebugInfo serialization (3/3) #2470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
8ec4d03 to
7df4114
Compare
bobbinth
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Thank you! I left some questions/comments inline.
| // Nodes with no decorators are valid | ||
| if decorator_ids.is_empty() && op_indptr_for_dec_ids.is_empty() { | ||
| // All node pointers must be 0 | ||
| if node_indptr_for_op_idx.iter().all(|&ptr| ptr == 0) { | ||
| return Ok(Self { | ||
| decorator_ids, | ||
| op_indptr_for_dec_ids, | ||
| node_indptr_for_op_idx, | ||
| }); | ||
| } else { | ||
| return Err(DecoratorIndexError::InternalStructure); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under what circumstances could this happen? That is, why would there be decorator storage initialized with nodes, but with no decorators?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This occurs when nodes exist in the MastForest but have no decorators attached to any operations. We've decided to use a dense representation:
- Serialize all indptr entries sequentially [0, 0, 0, 5, 5, 5, 5, 8, ...], including consecutive equal values that represent empty nodes (dense).
- Versus only serialize non-zero ranges as (node_id, start, end) tuples, skipping empty nodes entirely (sparse).
The sparse encoding overhead (storing node IDs + offsets) exceeds savings from skipping empty node entries — and the dense representation is simpler and more predictable. See the script at https://gist.github.com/huitseeker/7379e2eecffd7020ae577e986057a400 (linked in the PR)
55fd826 to
53ca96f
Compare
339c4cc to
3624f4a
Compare
84aca58 to
59bd1b9
Compare
3624f4a to
baaba49
Compare
59bd1b9 to
778060f
Compare
baaba49 to
c79276b
Compare
778060f to
7724be5
Compare
c79276b to
3733070
Compare
plafer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! MastForest serialization is much cleaner now 🙂
| // Validate before_enter CSR | ||
| let before_slice = self.node_indptr_for_before.as_slice(); | ||
| if !before_slice.is_empty() { | ||
| if before_slice[0] != 0 { | ||
| return Err("node_indptr_for_before must start at 0".to_string()); | ||
| } | ||
|
|
||
| for window in before_slice.windows(2) { | ||
| if window[0] > window[1] { | ||
| return Err(format!( | ||
| "node_indptr_for_before not monotonic: {} > {}", | ||
| window[0], window[1] | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| if *before_slice.last().unwrap() != self.before_enter_decorators.len() { | ||
| return Err(format!( | ||
| "node_indptr_for_before end {} doesn't match before_enter_decorators length {}", | ||
| before_slice.last().unwrap(), | ||
| self.before_enter_decorators.len() | ||
| )); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future PR: this CSR validation logic is copy/pasted for all instances for CSRs that we have. Ideally we would have a CsrMatrix type where we would write that once, and build all or concrete CSR types from it. It would look something like
pub struct CsrMatrix<Data, Idx> {
data: Vec<Data>,
indptr: IndexVec<Idx, usize>
}
pub struct NodeToDecoratorIds {
before_enter_decorators: CsrMatrix<DecoratorId, MastNodeId>,
after_exit_decorators: CsrMatrix<DecoratorId, MastNodeId>
}Ideally too we're able to reuse that type to define the "two-level CSR" that OpToDecoratorIds needs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7724be5 to
aad51b4
Compare
6066667 to
26ac7cc
Compare
bobbinth
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Thank you! I left some comments inline. After these are addressed, we should be good to merge.
| // Deserialize op_indptr_for_dec_ids | ||
| let op_indptr_u32: Vec<u32> = Deserializable::read_from(source)?; | ||
| let op_indptr_for_dec_ids: Vec<usize> = | ||
| op_indptr_u32.iter().map(|&idx| idx as usize).collect(); | ||
|
|
||
| // Deserialize node_indptr_for_op_idx | ||
| let node_indptr_u32: Vec<u32> = Deserializable::read_from(source)?; | ||
| let node_indptr_for_op_idx: Vec<usize> = | ||
| node_indptr_u32.iter().map(|&idx| idx as usize).collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why we want to write the pointers as u32 rather than usize? Is it because usize uses variable-length encoding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're invalid if >= u32::max. I'll hide that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usize serialization/deserialization we have will work fine cross-platform as long as the serialized values don't exceed the valid range for usize on the target platform (ref code here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I edited the comment which was misleading, this is not primarily about platforms (though that's a valid, if minor concern).
I thought that we had a limit (of u32::MAX) on the indices of decorators attached to a given op, and the indices of ops attached to a given node. Indeed the # of ops per node, and the total # of decorators are bound by u32::MAX. I therefore thought there are two equivalent ways of addressing the correctness issue:
- serializing / deserializing as usize, then checking every read value is bound by
u32::MAX, - serializing / deserializing as u32.
The later seemed simpler, allocation aside. I now realize that I am not sure any of those are actually bound by u32::MAX. We at least clearly can have values in op_indptr_for_dec_ids that can exceed u32::MAX, and because they are offsets in that array, same goes for the values of node_indptr_for_op_idx. See other comment.
Thanks for the pushback. Will edit.
| // Serialize indptr arrays as u32 for cross-platform compatibility | ||
| // (usize varies between 32-bit and 64-bit platforms) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our usize serialization is "cross-platform safe" (as long as the actual values stay within 32-bit range, and if not, we have an issue here anyway).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if not, we have an issue here anyway
I'm just not sure that's true. Assume for simplicity that you have precisely decorators.len() == u32::MAX / 100 (and that all quantities divide evenly). For each block, do the following:
- name
op_idxthe index of each operation in the block (we enforceop_idx <= u32::MAX), - if
op_idxis odd, attach all odd-indexed decorators in thedecoratorsarray to it, - symmetrically if
op_idxis even, attach all even-indexed decorators in thedecoratorsarray to it.
There's no trivial succinct encoding of that map, so decorator_ids.len() is decorators.len() / 2 * num_ops where num_ops is the total number of operations in the MastForest.
Each operation has to index into a u32::MAX / 200-long slice into the decorator_ids array (which values are all u32s). The 201st such slice has indexes ending at an offset > u32::MAX (the start and end indices of that slice are kept in op_indptr_for_dec_ids by definition). Now assume the MastForest has in aggregate over 200 operations — no one has flagrantly broken anything documented about the protocol, yet we have values in op_indptr_for_dec_ids above the u32 range.
It takes a bit more work to see the same issue at work in node_indptr_for_op_idx but it's the same idea.
f385c11 to
b2ba6a4
Compare
ce2fad2 to
842094c
Compare
7c4a99e to
c88a02a
Compare
842094c to
1aa41a1
Compare
c88a02a to
65a4972
Compare
…andling fixes Adds comprehensive round-trip tests for MastForest serialization/deserialization with direct DebugInfo handling, including edge cases for CSR (Compressed Sparse Row) format. Tests cover: - Basic MastForest with decorators roundtrip - Empty CSR arrays for nodes without decorators - Sentinel values in op_indptr for empty nodes after decorated nodes - Multiple operations and decorators per node - All node types with various decorator configurations CSR handling fixes: - Allow empty CSR arrays when nodes have no decorators - Add sentinel values to op_indptr for proper indexing
- Changed `OpToDecoratorIds::from_components` and `NodeToDecoratorIds::from_components` visibility from `pub` to `pub(super)` as they're only used internally for deserialization - Added serialization helper methods `write_into()` and `read_from()` to `OpToDecoratorIds` and `NodeToDecoratorIds` to break down large DebugInfo serialization implementation - Refactored DebugInfo (de)serialization to call helper methods instead of directly accessing CSR fields - Moved decorator storage tests to separate file (`decorator_storage/tests.rs`) following codebase pattern, reducing main file from 1669 to 871 lines - Added comment clarifying wire format compatibility for unused `_decorator_count` variable in MastForest deserialization
- Update validation doc comments to reflect valid edge cases - Update serialization format description in mod.rs - Simplify decorator_infos serialization using Vec::write_into
- Add Deserializable impl for DecoratorId to simplify deserialization - Simplify write_into/read_from in OpToDecoratorIds and NodeToDecoratorIds to use DecoratorId's serialization traits directly - Serialize indptr arrays as u32 (indices into u32-bounded arrays) - Update serialization format description in module docs - Add comment about procedure name validation at MastForest level - Update trusted source comment for deserialization - Remove unnecessary pub(super) from struct fields
Add TryFrom<Vec<T>> impl that validates length <= u32::MAX, enforcing the u32-indexed invariant. Deserialization uses this for validation.
The indptr arrays (op_indptr_for_dec_ids, node_indptr_for_op_idx, node_indptr_for_before, node_indptr_for_after) are indices that can exceed u32::MAX since decorator_ids can grow arbitrarily large due to decorator reuse across many operations. Use winter-utils Serializable/Deserializable impls for usize and the IndexVec impls to serialize these arrays directly.
65a4972 to
7dc6b6e
Compare
We were doing wasteful round-trip conversions in MastForest serialization. The old flow extracted decorator data from centralized storage, formatted it per-node for serialization, then reconstructed centralized storage during deserialization.
Solution
Serialize
DebugInfodirectly in CSR format. Deserialize it first, then use builders to reconstruct nodes that link to the already-populated decorator storage.Two distinct insertion paths:
add_to_forest(): registers decorators (normal assembly, part ofMastForestContributor)add_to_forest_relaxed(): assumes decorators already indebug_info(deserialization)Prior Work: Padded Serialization
Earlier PRs in this stack switched to padded operation serialization with batch metadata. Operations and decorator indices are written in their internal padded form for exact reconstruction.
This eliminated the "padding wrinkle" - complex conversions between padded and unpadded representations. Decorator indices in memory now match decorator indices on disk, making direct DebugInfo serialization straightforward.
Fixes #2448.