format: implicit chunk index addresses over the maximum chunk grid

libhdf5 allocates an implicit index's chunks for the whole maximum
extent and places chunk `scaled` at its row-major position in the
maximum chunk grid (H5D__none_idx_get_addr, max_down_chunks). The reader
used the current grid, so a dataset below its maximum shape with more
chunk columns at its maximum read other chunks' values from the second
chunk row on (h5py early allocation, fixed maxshape).

generate_implicit_chunks_in_grid takes the maximum dimensions;
generate_implicit_chunks keeps its signature (grid = current extent).
Regression: implicit_chunks_use_the_maximum_grid here, and
implicit_index_below_its_maximum_reads_like_libhdf5 (h5py file) with the
editor tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 16:35:06 -05:00
co-authored by Claude Opus 5.5
parent 8f59b2e1c2
commit 17201e279d
+47 -4
View File
@@ -933,16 +933,40 @@ pub fn generate_implicit_chunks(
dataset_dims: &[u64], dataset_dims: &[u64],
chunk_dimensions: &[u32], chunk_dimensions: &[u32],
element_size: u32, element_size: u32,
) -> Vec<ChunkInfo> {
generate_implicit_chunks_in_grid(
base_address,
dataset_dims,
dataset_dims,
chunk_dimensions,
element_size,
)
}
/// [`generate_implicit_chunks`] for a dataset whose maximum dimensions
/// (`max_dims`) exceed its current ones: libhdf5 allocates the chunks of
/// the whole maximum extent and places chunk `scaled` at its row-major
/// position in the *maximum* chunk grid (`H5D__none_idx_get_addr`,
/// `max_down_chunks`), so the current extent's chunks are not contiguous.
/// Only the chunks of the current extent are listed.
pub fn generate_implicit_chunks_in_grid(
base_address: u64,
dataset_dims: &[u64],
max_dims: &[u64],
chunk_dimensions: &[u32],
element_size: u32,
) -> Vec<ChunkInfo> { ) -> Vec<ChunkInfo> {
let rank = chunk_dimensions.len(); let rank = chunk_dimensions.len();
let chunk_byte_size: u64 = let chunk_byte_size: u64 =
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64; chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
let mut num_chunks_per_dim = Vec::with_capacity(rank); let mut num_chunks_per_dim = Vec::with_capacity(rank);
let mut grid_per_dim = Vec::with_capacity(rank);
for d in 0..rank { for d in 0..rank {
let ds = dataset_dims[d];
let ch = chunk_dimensions[d] as u64; let ch = chunk_dimensions[d] as u64;
num_chunks_per_dim.push(ds.div_ceil(ch)); let n = dataset_dims[d].div_ceil(ch);
num_chunks_per_dim.push(n);
grid_per_dim.push(max_dims.get(d).map_or(n, |m| m.div_ceil(ch)).max(n));
} }
let total_chunks: u64 = num_chunks_per_dim.iter().product(); let total_chunks: u64 = num_chunks_per_dim.iter().product();
@@ -951,18 +975,22 @@ pub fn generate_implicit_chunks(
for linear_idx in 0..total_chunks { for linear_idx in 0..total_chunks {
let mut offsets = vec![0u64; rank]; let mut offsets = vec![0u64; rank];
let mut remaining = linear_idx; let mut remaining = linear_idx;
let mut grid_idx = 0u64;
let mut down = 1u64;
for d in (0..rank).rev() { for d in (0..rank).rev() {
let nchunks = num_chunks_per_dim[d]; let nchunks = num_chunks_per_dim[d];
let chunk_idx = remaining % nchunks; let chunk_idx = remaining % nchunks;
remaining /= nchunks; remaining /= nchunks;
offsets[d] = chunk_idx * chunk_dimensions[d] as u64; offsets[d] = chunk_idx * chunk_dimensions[d] as u64;
grid_idx = grid_idx.saturating_add(chunk_idx.saturating_mul(down));
down = down.saturating_mul(grid_per_dim[d]);
} }
chunks.push(ChunkInfo { chunks.push(ChunkInfo {
chunk_size: chunk_byte_size as u32, chunk_size: chunk_byte_size as u32,
filter_mask: 0, filter_mask: 0,
offsets, offsets,
address: base_address + linear_idx * chunk_byte_size, address: base_address.saturating_add(grid_idx.saturating_mul(chunk_byte_size)),
}); });
} }
@@ -1153,9 +1181,13 @@ pub fn list_chunks(
(4, Some(2)) => { (4, Some(2)) => {
// Implicit index — use spatial chunk dims only // Implicit index — use spatial chunk dims only
let spatial_chunk_dims: &[u32] = &chunk_dimensions[..rank]; let spatial_chunk_dims: &[u32] = &chunk_dimensions[..rank];
generate_implicit_chunks( generate_implicit_chunks_in_grid(
addr, addr,
&dataspace.dimensions, &dataspace.dimensions,
dataspace
.max_dimensions
.as_deref()
.unwrap_or(&dataspace.dimensions),
spatial_chunk_dims, spatial_chunk_dims,
elem_size as u32, elem_size as u32,
) )
@@ -3015,6 +3047,17 @@ mod tests {
} }
} }
/// A dataset below its maximum extent: libhdf5 lays chunks out over the
/// maximum chunk grid, so row 1 starts after a whole maximum row (here
/// 4 chunks), not after the current row of 3.
#[test]
fn implicit_chunks_use_the_maximum_grid() {
let chunks = generate_implicit_chunks_in_grid(0x100, &[2, 3], &[4, 4], &[1, 1], 4);
let addrs: Vec<u64> = chunks.iter().map(|c| (c.address - 0x100) / 4).collect();
assert_eq!(addrs, vec![0, 1, 2, 4, 5, 6]);
assert_eq!(chunks[3].offsets, vec![1, 0]);
}
#[test] #[test]
fn implicit_chunks_partial_last() { fn implicit_chunks_partial_last() {
// 25 elements, chunk size 10 => 3 chunks (last partial) // 25 elements, chunk size 10 => 3 chunks (last partial)