Skip to content

Commit 6c4965b

Browse files
authored
Improve rechunk stats and visualizations (#934)
1 parent 516fb2f commit 6c4965b

6 files changed

Lines changed: 173 additions & 16 deletions

File tree

cubed/array_api/array_object.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from cubed.backend_array_api import namespace as nxp
2323
from cubed.core.array import CoreArray
2424
from cubed.core.ops import elemwise
25-
from cubed.utils import itemsize, memory_repr
25+
from cubed.utils import memory_repr
2626

2727
ARRAY_SVG_SIZE = (
2828
120 # cubed doesn't have a config module like dask does so hard-code this for now
@@ -56,7 +56,7 @@ def _repr_html_(self):
5656

5757
if not math.isnan(self.nbytes):
5858
nbytes = memory_repr(self.nbytes)
59-
cbytes = memory_repr(math.prod(self.chunksize) * itemsize(self.dtype))
59+
cbytes = memory_repr(self.cbytes)
6060
else:
6161
nbytes = "unknown"
6262
cbytes = "unknown"

cubed/core/array.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import math
12
from functools import reduce
23
from operator import mul
34
from typing import Literal, TypeVar
@@ -103,6 +104,11 @@ def nbytes(self) -> int:
103104
"""Number of bytes in array"""
104105
return self.size * itemsize(self.dtype)
105106

107+
@property
108+
def cbytes(self) -> int:
109+
"""Number of bytes in a chunk"""
110+
return math.prod(self.chunksize) * itemsize(self.dtype)
111+
106112
@property
107113
def nchunks(self) -> int:
108114
"""Number of chunks in array"""

cubed/core/rechunk.py

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@
33
import logging
44
import warnings
55
from collections.abc import Sequence
6-
from dataclasses import dataclass
7-
from math import floor, prod
6+
from dataclasses import dataclass, field
7+
from math import ceil, floor, prod
8+
from typing import TYPE_CHECKING
89

910
import numpy as np
1011

1112
from cubed.core.plan import FinalizedPlan
1213
from cubed.types import T_RegularChunks, T_Shape
13-
from cubed.utils import (
14-
normalize_chunks,
15-
)
14+
from cubed.utils import normalize_chunks
1615
from cubed.vendor.rechunker.algorithm import (
1716
MAX_STAGES,
1817
ExcessiveIOWarning,
@@ -22,6 +21,9 @@
2221
consolidate_chunks,
2322
)
2423

24+
if TYPE_CHECKING:
25+
from cubed.array_api.array_object import Array
26+
2527
logger = logging.getLogger(__name__)
2628

2729

@@ -253,19 +255,46 @@ class RechunkCopy:
253255
target_chunks: T_RegularChunks
254256
"""The chunks of the target array for this copy operation."""
255257

256-
source_aligned: bool | None = None
258+
num_input_blocks: int = field(init=False)
259+
"""The number of input blocks read from the source."""
260+
261+
num_output_blocks: int = field(init=False)
262+
"""The number of output blocks written to the target."""
263+
264+
source_aligned: bool = field(init=False)
257265
"""Are copy chunks aligned with source chunks?"""
258266

259-
target_aligned: bool | None = None
267+
target_aligned: bool = field(init=False)
260268
"""
261269
Are copy chunks aligned with target chunks?
262270
If not then irregular chunking must be used.
263271
"""
264272

273+
def __post_init__(self):
274+
self.num_input_blocks = prod(
275+
ceil(cc / sc) for cc, sc in zip(self.copy_chunks, self.source_chunks)
276+
)
277+
self.num_output_blocks = prod(
278+
ceil(cc / tc) for cc, tc in zip(self.copy_chunks, self.target_chunks)
279+
)
280+
self.source_aligned = all(
281+
(cc == n) or (cc % sc == 0)
282+
for n, sc, cc in zip(self.shape, self.source_chunks, self.copy_chunks)
283+
)
284+
self.target_aligned = all(
285+
(cc == n) or (cc % tc == 0)
286+
for n, tc, cc in zip(self.shape, self.target_chunks, self.copy_chunks)
287+
)
288+
265289

266290
@dataclass
267291
class RechunkPlan:
292+
arrays: list["Array"]
268293
copy_ops: list[RechunkCopy]
294+
regular: bool = field(init=False)
295+
296+
def __post_init__(self):
297+
self.regular = not any(not copy_op.target_aligned for copy_op in self.copy_ops)
269298

270299
def _repr_html_(self):
271300
from cubed.diagnostics.widgets import get_template
@@ -275,20 +304,60 @@ def _repr_html_(self):
275304
for copy_op in self.copy_ops:
276305
row = (
277306
copy_op,
278-
svg(normalize_chunks(copy_op.source_chunks, shape=copy_op.copy_chunks)),
279-
svg(normalize_chunks(copy_op.target_chunks, shape=copy_op.copy_chunks)),
307+
svg(
308+
normalize_chunks(copy_op.source_chunks, shape=copy_op.copy_chunks),
309+
size=80,
310+
),
311+
svg(
312+
normalize_chunks(copy_op.target_chunks, shape=copy_op.copy_chunks),
313+
size=80,
314+
),
280315
)
281316
table.append(row)
282317

283318
return get_template("rechunk_plan.j2").render(table=table)
284319

320+
@property
321+
def result(self):
322+
return self.arrays[-1]
323+
324+
@property
325+
def array_view(self):
326+
return RechunkPlanArrayView(self.arrays)
327+
328+
@property
329+
def stats(self):
330+
return RechunkPlanStats.from_plan(self, self.result.plan())
331+
332+
333+
@dataclass
334+
class RechunkPlanArrayView:
335+
arrays: list["Array"]
336+
337+
def _repr_html_(self):
338+
from cubed.diagnostics.widgets import get_template
339+
340+
table = []
341+
for array in self.arrays:
342+
row = (
343+
array,
344+
array.to_svg(size=120),
345+
)
346+
table.append(row)
347+
348+
return get_template("rechunk_arrays.j2").render(table=table)
349+
285350

286351
@dataclass
287352
class RechunkPlanStats:
288353
num_copy_ops: int
289354
num_tasks: int
290355
total_nbytes_written: int
356+
total_task_iops: int
291357
max_task_iops: int
358+
max_task_input_blocks: int
359+
max_task_output_blocks: int
360+
regular: bool
292361

293362
@classmethod
294363
def from_plan(cls, rechunk_plan: RechunkPlan, plan: FinalizedPlan):
@@ -302,23 +371,51 @@ def from_plan(cls, rechunk_plan: RechunkPlan, plan: FinalizedPlan):
302371
+ d["pipeline"].config.num_output_blocks[0]
303372
for _, d in rechunks
304373
]
374+
total_task_iops = sum(
375+
d["primitive_op"].num_tasks
376+
* (
377+
d["pipeline"].config.num_input_blocks[0]
378+
+ d["pipeline"].config.num_output_blocks[0]
379+
)
380+
for _, d in rechunks
381+
)
382+
num_task_input_blocks = [
383+
d["pipeline"].config.num_input_blocks[0] for _, d in rechunks
384+
]
385+
num_task_output_blocks = [
386+
d["pipeline"].config.num_output_blocks[0] for _, d in rechunks
387+
]
305388

306389
return cls(
307390
num_copy_ops=len(rechunk_plan.copy_ops),
308391
num_tasks=plan.num_tasks,
309392
total_nbytes_written=plan.total_nbytes_written,
393+
total_task_iops=total_task_iops,
310394
max_task_iops=max(num_task_iops),
395+
max_task_input_blocks=max(num_task_input_blocks),
396+
max_task_output_blocks=max(num_task_output_blocks),
397+
regular=rechunk_plan.regular,
311398
)
312399

313400

314401
def rechunk_plan(x, chunks, *, min_mem=None, allow_irregular=True):
315402
from cubed.core.ops import _rechunk_plan
316403

404+
out = x
405+
arrays = [x]
317406
copy_ops = []
318407
source_chunks = x.chunksize
319408
for copy_chunks, target_chunks in _rechunk_plan(
320409
x, chunks, min_mem=min_mem, allow_irregular=allow_irregular
321410
):
411+
from .ops import _rechunk
412+
413+
out = _rechunk(out, copy_chunks, target_chunks, allow_irregular=allow_irregular)
414+
arrays.append(out)
322415
copy_ops.append(RechunkCopy(x.shape, source_chunks, copy_chunks, target_chunks))
323416
source_chunks = target_chunks
324-
return RechunkPlan(copy_ops)
417+
418+
# final output must have regular chunking
419+
assert copy_ops[-1].target_aligned
420+
421+
return RechunkPlan(arrays, copy_ops)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<table>
2+
<tbody>
3+
{% for row in table %}
4+
<tr>
5+
<td>
6+
<table>
7+
<thead>
8+
<tr>
9+
<td> </td>
10+
<th> Array </th>
11+
<th> Chunk </th>
12+
</tr>
13+
</thead>
14+
<tbody>
15+
<tr>
16+
<th> Bytes </th>
17+
<td> {{ row[0].nbytes | memory_repr }} </td>
18+
<td> {{ row[0].cbytes | memory_repr }} </td>
19+
</tr>
20+
<tr>
21+
<th> Shape </th>
22+
<td> {{ row[0].shape }} </td>
23+
<td> {{ row[0].chunksize }} </td>
24+
</tr>
25+
<tr>
26+
<th> Count </th>
27+
<td> {{ 1 }} </td>
28+
<td> {{ row[0].npartitions }} Chunks </td>
29+
</tr>
30+
<tr>
31+
<th> Type </th>
32+
<td> {{ row[0].dtype }} </td>
33+
<td> {{ arrtype }} </td>
34+
</tr>
35+
</tbody>
36+
</table>
37+
</td>
38+
<td>
39+
{{row[1]}}
40+
</td>
41+
</tr>
42+
{% endfor %}
43+
</tbody>
44+
</table>

cubed/diagnostics/widgets/templates/rechunk_plan.j2

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232
<th> Target chunks </th>
3333
<td> {{ row[0].target_chunks }} </td>
3434
</tr>
35+
<tr>
36+
<th> Num input blocks </th>
37+
<td> {{ row[0].num_input_blocks }} </td>
38+
</tr>
39+
<tr>
40+
<th> Num output blocks </th>
41+
<td> {{ row[0].num_output_blocks }} </td>
42+
</tr>
43+
<tr>
44+
<th> Target aligned </th>
45+
<td> {{ row[0].target_aligned }} </td>
46+
</tr>
3547
</tbody>
3648
</table>
3749
</td>

cubed/tests/test_rechunk.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from cubed.backend_array_api import namespace as nxp
77
from cubed.core.ops import _store_array, split_chunksizes
88
from cubed.core.rechunk import (
9-
RechunkPlanStats,
109
calculate_regular_stage_chunks,
1110
multistage_regular_rechunking_plan,
1211
multspace,
@@ -178,11 +177,10 @@ def test_rechunk_plan_viz():
178177
assert len(rplan.copy_ops) == 2
179178
# check generating the repr doesn't raise an exception
180179
rplan._repr_html_()
180+
rplan.array_view._repr_html_()
181181

182182
# check stats (without running the rechunk)
183-
b = a.rechunk(target_chunks)
184-
plan = b.plan()
185-
stats = RechunkPlanStats.from_plan(rplan, plan)
183+
stats = rplan.stats
186184
assert stats.num_copy_ops == 2
187185
assert stats.max_task_iops == 23
188186

0 commit comments

Comments
 (0)