cuslide2: add pyramidal OME-TIFF semantics and nvImageCodec 0.9 readiness - #1122
cuslide2: add pyramidal OME-TIFF semantics and nvImageCodec 0.9 readiness#1122cdinea wants to merge 4 commits into
Conversation
854582c to
70e0e73
Compare
Update nvImageCodec dependency constraints to 0.9.x, add parser/decoder compatibility fallbacks for 0.9 runtime behavior, and relax legacy SubfileType assumptions so stacked OME-style TIFF pages load successfully in cuslide2 validation.
|
/ok to test 57aa615 |
| exec_params.device_allocator = nullptr; | ||
| exec_params.pinned_allocator = nullptr; | ||
| exec_params.max_num_cpu_threads = compute_max_decoder_threads(); | ||
| exec_params.max_num_cpu_threads = 0; |
There was a problem hiding this comment.
I'm wondering why we're setting this to 0 now.
There was a problem hiding this comment.
Yeah this would recover the overcontention issues which were fixed in the last release.
There was a problem hiding this comment.
Good catch — unintentional, and Lev is right that it reintroduces the over-contention regression.
nvimgcodec.h documents max_num_cpu_threads = 0 as "default value equal to number of cpu cores", so this decoder was silently taking a full-core CPU threadpool. That was harmless when it was CPU-only and metadata-only, but it now backs the primary decode path (NVIMGCODEC_DEVICE_CURRENT, all backends), so with N dataloader workers you get N x cores threads.
The 0 is a leftover from the pre-promotion shape of this code; the rebase dropped compute_max_decoder_threads() along with it, which is the ~76-line shift in this hunk.
Fixed in 9bd44b8 — heuristic is back to min(max(1, hardware_concurrency / 4), 8), still overridable via CUCIM_MAX_DECODER_THREADS (>0 = exact count, 0 = explicit opt-in to the nvImageCodec default).
| // Current limitation (nvImageCodec v0.6.0): | ||
| // - codec_name returns "tiff" (container format) not compression type | ||
| // - Individual TIFF tags not exposed through metadata API | ||
| // - Only vendor-specific metadata blobs available (MED_APERIO, MED_PHILIPS, etc.) | ||
| // |
There was a problem hiding this comment.
I am wondering if nvImageCodec v0.9 still has these limitation.
There was a problem hiding this comment.
We do support generic tags in the API, here is python example: https://docs.nvidia.com/cuda/nvimagecodec/samples/metadata.html#Generic-tiff-tag-reading
iirc this was already supported in 0.8? Lev can know more details on this and how to querry the tags
There was a problem hiding this comment.
This has been supported since 0.8, but only with a GPU decoder.
There was a problem hiding this comment.
You're right, and thanks @mkepa-nv for the pointer — the comment was stale. Individual TIFF tags are queried unconditionally via NVIMGCODEC_METADATA_KIND_TIFF_TAG in extract_tiff_tags(), which runs on the line directly above, so the claim contradicted the adjacent code.
Removed in 9bd44b8. I kept only the part that's still true: codec_name reports the container format rather than the compression type, so compression is inferred from the tags below.
| // WORKAROUND for nvImageCodec 0.6.0: Philips TIFF metadata limitation | ||
| // ======================================================================== | ||
| // nvImageCodec 0.6.0 does NOT expose: | ||
| // 1. Individual TIFF tags (SOFTWARE, ImageDescription, etc.) | ||
| // 2. Philips format detection for some files | ||
| // | ||
|
|
There was a problem hiding this comment.
Is this still specific to nvImageCodec v0.9.0, or does the code also work with nvImageCodec v0.6.0?
There was a problem hiding this comment.
That block was a stale v0.6.0 workaround note describing limitations that no longer apply — removed entirely in 9bd44b8. The surrounding code path is not v0.6.0-compatible anyway, since it depends on the 0.7.0+ TIFF tag metadata API.
| @@ -1,5 +1,5 @@ | |||
| /* | |||
| * SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION. | |||
| * SPDX-FileCopyrightText: Copyright (c) 2020-2021, NVIDIA CORPORATION. | |||
There was a problem hiding this comment.
| * SPDX-FileCopyrightText: Copyright (c) 2020-2021, NVIDIA CORPORATION. | |
| * SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION. |
There was a problem hiding this comment.
Fixed. This one had actually gone backwards — the base was already 2020-2026 and the branch reverted it to 2021
| if (NOT TARGET deps::nvimgcodec) | ||
| set(NVIMGCODEC_SONAME_CANDIDATES | ||
| "libnvimgcodec.so.0.9.0" | ||
| "libnvimgcodec.so.0.8.0" |
There was a problem hiding this comment.
In the conda yaml file you set requirements >= 0.9 for nvimagecodec. Why do we need 0.8 here then?
There was a problem hiding this comment.
You're right, that 0.8.0 line shouldn't be there — it's left over from when this branch still supported 0.8. Everything else already asks for 0.9: dependencies.yaml, the conda environment files, conda_build_config.yaml and pyproject.toml are all >=0.9.0,<0.10.0, and the vendored headers say NVIMGCODEC_VER_MINOR 9. Dropped it from this file and the cuslide2 copy.
Digging into it though, removing that line isn't quite enough. The list still ends with libnvimgcodec.so.0 and libnvimgcodec.so, and those symlinks point at whatever major-0 build happens to be installed — so on a machine with only 0.8 we'd still pick it up without noticing. Combined with the two things you spotted elsewhere (limit_images gone in 0.9, no compile-time version checks), that would blow up at runtime instead of failing the build, which is a much worse way to find out.
So I added a check at configure time: it reads NVIMGCODEC_VER_MAJOR/MINOR out of the nvimgcodec_version.h we discover, errors below 0.9, warns if it's outside the pinned range, and skips quietly when the header isn't there so wheel and system installs keep working. The find_package branch now looks at nvimgcodec_VERSION too — it wasn't checking anything at all before.
On consolidating the two nvimgcodec.cmake files: fair point, and the check I just added is now duplicated as well. I'd rather do that separately than grow this PR — the two files get picked up through different superbuild_depend() search paths (cuslide2 via SUPERBUILD_ADDITIONAL_DEPS_DIRS, the other via CMAKE_SUPERBUILD_DEPS_ROOT_DIR, which points at the cuslide plugin's cmake dir and has no nvimgcodec entry), so merging them takes a little care not to bbreak standalone plugin builds.
|
|
||
| set(NVIMGCODEC_SONAME_CANDIDATES | ||
| "libnvimgcodec.so.0.9.0" | ||
| "libnvimgcodec.so.0.8.0" |
There was a problem hiding this comment.
Similar question as above. Also maybe the cmake structure can be simplified so nvimagecodec search is done in a single place? But this is a question for a future pull request
| nvimgcodecCodeStreamView_t view{}; | ||
| view.struct_type = NVIMGCODEC_STRUCTURE_TYPE_CODE_STREAM_VIEW; | ||
| view.struct_size = sizeof(nvimgcodecCodeStreamView_t); | ||
| view.struct_size = offsetof(nvimgcodecCodeStreamView_t, limit_images); |
There was a problem hiding this comment.
This doesn't look right. Why would we ask for offset of struct member instead of the whole struct size? And also limit_images was removed in 0.9 so I have no idea how is this working right now
| uint32_t num_channels = ifd_info.num_channels > 0 ? ifd_info.num_channels : 3; | ||
| size_t row_stride = width * num_channels; | ||
| uint32_t num_channels = decode_spec.num_channels; | ||
| size_t row_stride = width * num_channels * decode_spec.bytes_per_sample; |
There was a problem hiding this comment.
This can overflow for large width, I would static cast widt to size_t if you store it in that type anyway
| size_t row_stride = region.width * num_channels; | ||
| const DecodePixelSpec decode_spec = resolve_decode_pixel_spec(ifd_info); | ||
| uint32_t num_channels = decode_spec.num_channels; | ||
| size_t row_stride = region.width * num_channels * decode_spec.bytes_per_sample; |
| ifd->height_ = height_l0 / downsample; | ||
| } | ||
| // Fix width and height of IFD | ||
| ifd->width_ = width_l0 / downsample; |
| // | ||
|
|
||
| // 34 is from `Attribute Name="PIM_DP_IMAGE_DATA"` | ||
| char* data_ptr = const_cast<char*>(image_desc_cstr) + node_offset + 34; |
There was a problem hiding this comment.
What if this is outside of the image_desc_cstr?
| auto add_owned_channel_name = [&resource, &channel_names](const std::string& name) { | ||
| void* raw = resource.allocate(name.size() + 1, alignof(char)); | ||
| auto* buf = static_cast<char*>(raw); | ||
| memcpy(buf, name.c_str(), name.size() + 1); |
There was a problem hiding this comment.
I would just make the channel_names to store string instead of the string_view. That vector is the owner of the names, so this even sounds like a more correct approach.
| gpu_time = time.time() - start | ||
| gpu_np = _to_numpy(gpu_region) | ||
| print(f" GPU level {level}: {gpu_np.shape}, {gpu_np.dtype}, {gpu_time:.4f}s") | ||
| if gpu_np.shape != cpu_np.shape: |
There was a problem hiding this comment.
shape check is not enough. I would check the pixels too. And maybe you should check that each level have a correct dimensions (that they decrease with correct ratio for each level done) - otherwise you will not be sure that parsing is working as expected.
| print(f" ⚠️ GPU plane validation skipped/failed: {e}") | ||
|
|
||
|
|
||
| def _validate_batch_decode(img, level_dims): |
There was a problem hiding this comment.
You added a bunch of code for higher resolution decode (with dtype uint 16) - I would add test for such case as well
max_num_cpu_threads = 0 requests one CPU thread per core from nvImageCodec's default executor. That was harmless when this decoder was CPU-only and used solely for metadata extraction, but it now backs the primary decode path (NVIMGCODEC_DEVICE_CURRENT, all backends enabled), so every worker process allocated a full-core threadpool and multi-worker runs regressed into the thread over-contention this heuristic was originally added to prevent. Restore compute_max_decoder_threads(), which bounds the pool to min(max(1, hardware_concurrency / 4), 8) and stays overridable via CUCIM_MAX_DECODER_THREADS. Also drop the stale nvImageCodec v0.6.0 limitation comments: individual TIFF tags are queried unconditionally through NVIMGCODEC_METADATA_KIND_TIFF_TAG by extract_tiff_tags(), so the claim that they are unavailable contradicted the adjacent code.
What this PR changes
ome_xml.h/.cpp)TiffDatacompanion-file referencesScope notes
Validation
cucim.kit.cuslide2successfully in the prepared build environmentcuslide2_tests