-
Notifications
You must be signed in to change notification settings - Fork 92
feat: add table_shard_size_bytes to control the zarr shard size of tables
#1199
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
base: main
Are you sure you want to change the base?
Changes from all commits
dbece29
9a2e82f
c74ccee
bbffcac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |
| from ome_zarr.format import Format | ||
| from packaging.version import Version | ||
|
|
||
| from spatialdata._io._utils import _resolve_zarr_store | ||
| from spatialdata._io._utils import _resolve_zarr_store, _table_shard_budget, _validate_table_shard_size_bytes | ||
| from spatialdata._io.exceptions import FormatVersionUnknownError, WritingToZarrV2DeprecationWarning | ||
| from spatialdata._io.format import ( | ||
| CurrentTablesFormat, | ||
|
|
@@ -61,6 +61,8 @@ def write_table( | |
| group_type: str = "ngff:regions_table", | ||
| element_format: Format = CurrentTablesFormat(), | ||
| convert_strings_to_categoricals: bool = False, | ||
| *, | ||
| shard_size_bytes: int | None = None, | ||
| ) -> None: | ||
| """ | ||
| Write a table to a Zarr store. | ||
|
|
@@ -80,7 +82,18 @@ def write_table( | |
| convert_strings_to_categoricals | ||
| If True, convert string columns to categoricals before writing. | ||
| Note that this will have a side effect of modifying dtypes of the input table in place. | ||
| shard_size_bytes | ||
| The target size in bytes of uncompressed data for a single zarr shard of every array of the table. If `None` | ||
| (default), no shard budget is requested and the write is left entirely to the backend defaults. Requires a | ||
| zarr v3 table format, zarr >= 3.1.6 and an anndata that supports zarr v3 auto-sharding. | ||
|
|
||
| Raises | ||
| ------ | ||
| TableWriteOptionsError | ||
| If `shard_size_bytes` is not a positive `int`, or if the active backend cannot honour a shard budget. | ||
| """ | ||
| _validate_table_shard_size_bytes(shard_size_bytes, tables_zarr_format=element_format.zarr_format) | ||
|
|
||
| if element_format.zarr_format == 2: | ||
| warnings.warn( | ||
| message=WritingToZarrV2DeprecationWarning.message, category=WritingToZarrV2DeprecationWarning, stacklevel=2 | ||
|
|
@@ -98,26 +111,27 @@ def write_table( | |
| if element_format not in TablesFormats.values(): | ||
| raise FormatVersionUnknownError(element_type="table", version_encountered=element_format) | ||
|
|
||
| if element_format.zarr_format == 3 and Version(version("anndata")) >= Version("0.13"): | ||
| # `write_zarr` in anndata v0.13 and above can only write to zarr v3 | ||
| # solution of passing resolved store directly roughly based on: | ||
| # https://github.com/scverse/anndata/issues/1548#issuecomment-2199801855 | ||
| with _table_shard_budget(shard_size_bytes): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just like to be absolutely sure that this is the only way to do this. Temporarily setting a global variable is a very dangerous design, even with the context managers (e.g.: how do we even know we're not already inside a context? what happens on multithreaded applications? etc), so if there is any way we could pass these arguments to the a function call, I'd much much much prefer that.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked this before, and here is the short version: for a byte budget it is the only way I could find, and it is the way anndata itself already uses.
The per-call alternative is an explicit The real fix for your concern is an argument on anndata's side, something like a shard budget on |
||
| if element_format.zarr_format == 3 and Version(version("anndata")) >= Version("0.13"): | ||
| # `write_zarr` in anndata v0.13 and above can only write to zarr v3 | ||
| # solution of passing resolved store directly roughly based on: | ||
| # https://github.com/scverse/anndata/issues/1548#issuecomment-2199801855 | ||
|
|
||
| # resolve the store from the group | ||
| resolved_store = _resolve_zarr_store(table_group) | ||
| # resolve the store from the group | ||
| resolved_store = _resolve_zarr_store(table_group) | ||
|
|
||
| # Write the table to the path of the table group | ||
| table.write_zarr( | ||
| store=resolved_store, | ||
| consolidate_metadata=False, | ||
| convert_strings_to_categoricals=convert_strings_to_categoricals, | ||
| ) | ||
| # Write the table to the path of the table group | ||
| table.write_zarr( | ||
| store=resolved_store, | ||
| consolidate_metadata=False, | ||
| convert_strings_to_categoricals=convert_strings_to_categoricals, | ||
| ) | ||
|
|
||
| else: | ||
| if convert_strings_to_categoricals: | ||
| table.strings_to_categoricals() | ||
| else: | ||
| if convert_strings_to_categoricals: | ||
| table.strings_to_categoricals() | ||
|
|
||
| write_adata(group, name, table) | ||
| write_adata(group, name, table) | ||
|
|
||
| # Re-fetch the group before setting the attributes below: the handle obtained above was cached while the group | ||
| # was still empty, and zarr writes attributes as a whole document based on the handle's cached view, so writing | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
I know we're trying to be nice here by catering to many versions of anndata (and zarr!) but I really dislike that we essentially lie to our users on the function signature, only to immediately disappoint by throwing an exception if the versions of zarr and/or anndata aren't what we need. We also completely defeat the type checker's ability to tell if the arguments are good or not.
One way around it would be to name those parameters as something like
table_shard_size_bytes_hint(note the "hint" at the end); This makes it clear that they may or may not apply and we can just do nothing if that feature isn't supported.Alternatively, we could create the type
TableShardBudget, with a method likeTableShardBudget.try_create(...), which is clearly visibly fallible, and would go through the validation logic in this function. This way if a client fails to get aTableShardBudget, then they can react accordingly (and locally to their code!), and all functions that use the budget don't have to re-validate. And you could also make theTableShardBudgetbe itself the context manager.Maybe there is a way to have different signatures depending on what dependencies we have, but that would have strange impacts in our versioning scheme, so I'm skeptical that this could work.
Curious to see what other people think
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.
I see the problem, and I think there might be a third option to bump the floors. This should remove rather than rename around it.
The two version checks only because
pyproject.tomlstill saysanndata>=0.9.1andzarr.=3.0.0. The pieces this argument needs are older than they look:anndata.settings.auto_shard_zarr_v3exists since anndata 0.12.5 (anndata#2167)zarr.configexists since anndata 0.12.14 (anndata#2427)The test matrix here already only runs anndata 0.12 and 0.13, and dask is pinned at
>=2026.3.0, soanndata>=0.12.14andzarr>=3.1.6would not be unusual. With those floors both version checks are deleted and the signature is honest on every supported install.What remains is for zarr v2 tabe format, and I suggest we keep raising there, since it is not a dependency proble, but a user asking for a format without sharding, and it already carries a deprecation warning. It also raises at the top of
write()before a single element is written, so nobodoy ends up with a half-written store.On the two alternatives: I prefer not to do
_hint, as a silently ignored budgest is exactly the failure that motivate the issue, a table landing as a few hundred thousand files with nobody told. If you prefer not to touch the pins, I can do theTableShardBudgetobject instead. I will then validate in__init__and raise rather than atry_createthat returnsNone, but that is a detail. Tell me which and I will push it.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.
One caveat against my own suggestion, since it changes how much the floor bump actually buys.
I checked the versions against the tags rather than trusting the PR numbers:
auto_shard_zarr_v3is absent in anndata 0.12.4 and present in 0.12.5;target_shard_size_bytesis absent in 0.12.13 and present in 0.12.14; and thehas_auto_shard_sizeyield-to-caller branch is intact at 0.12.14. So the floors themselves are right.But the matrix pins
anndata>=0.12,<0.13, which resolves to the newest 0.12.x, 0.12.19 today. Ananndata>=0.12.14floor would therefore never actually be exercised by CI. It would be a supported-version claim we do not test. That is true of whatever floor we pick rather than an argument against this one, and it does not change my preference, but you should weigh it now rather than hear it from me later.Still happy to do
TableShardBudgetinstead if you would rather not move the pins. Either way I would keep the zarr v2 case raising, since that is a user asking for a format without sharding rather than a dependency problem.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.
Small correction to my own caveat: the
anndata>=0.12,<0.13pin is in the hatchtest-anndata-pandasmatrix inpyproject.toml, and no workflow runs that matrix. The GitHub legs douv sync --group=test, so every one of them resolves the newest anndata, 0.13.3.post0 today, and the prerelease leg installs git main. So anndata 0.12 is not exercised by CI at any version, and a floor bump neither adds nor removes coverage. Same conclusion, stated more precisely.