From 5ed8cfdd02aa54e7dd4c4ac6e6fa88f4d379ba79 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 18:28:16 -0400 Subject: [PATCH 1/3] Project native image ABI at declared processing boundary Retain image context for existing registered contract wrappers, and derive native callable arguments through the shared ABI owner. Refs #1083. --- .../backends/lib_registry/unified_registry.py | 7 ++-- .../test_unified_registry_runtime_payloads.py | 41 ++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/openhcs/processing/backends/lib_registry/unified_registry.py b/openhcs/processing/backends/lib_registry/unified_registry.py index b400b7d632..9b2b6643f9 100644 --- a/openhcs/processing/backends/lib_registry/unified_registry.py +++ b/openhcs/processing/backends/lib_registry/unified_registry.py @@ -1141,9 +1141,10 @@ def supports_measurement_image_batch( def main_flow_call_argument( self, callable_contract: "CallableContract", source_payload: Any, ) -> Any: - """Keep the declared plane domain until the PURE_2D slicer consumes it.""" - del callable_contract - return source_payload + """Retain plane context only for the declared contract execution wrapper.""" + if callable_contract.raw_processing_function is not None: + return source_payload + return super().main_flow_call_argument(callable_contract, source_payload) def execute(self, registry, func, image, *args, **kwargs): return registry.execute_pure_2d(func, image, *args, **kwargs) diff --git a/tests/unit/test_unified_registry_runtime_payloads.py b/tests/unit/test_unified_registry_runtime_payloads.py index acafac1954..c858cefc70 100644 --- a/tests/unit/test_unified_registry_runtime_payloads.py +++ b/tests/unit/test_unified_registry_runtime_payloads.py @@ -19,12 +19,14 @@ from openhcs.core.callable_contract import CallableContract, callable_request from openhcs.core.config import LazyDtypeConfig +from openhcs.core.function_patterns import compile_function_pattern from openhcs.core.memory import MEMORY_TYPE_NUMPY from openhcs.core.memory.decorators import numpy from openhcs.core.runtime_array_values import RuntimeArrayPayload from openhcs.core.runtime_image_values import ( ImageMetadataPayload, ImagePayloadMetadata, + ImagePayloadMetadataCarrier, image_payload_data, image_payload_mask, image_payload_metadata, @@ -213,7 +215,7 @@ def add_one(image: ImageMetadataPayload) -> ImageMetadataPayload: payload, ) - assert isinstance(result, ImageMetadataPayload) + assert isinstance(result, ImagePayloadMetadataCarrier) np.testing.assert_array_equal(image_payload_data(result), stack + 1) assert seen_paths == ["z0.tif", "z1.tif"] assert image_payload_metadata(result).source_image_provenance_planes.paths == ( @@ -222,6 +224,43 @@ def add_one(image: ImageMetadataPayload) -> ImageMetadataPayload: ) +@pytest.mark.parametrize("slice_by_slice", (True, False)) +def test_native_flexible_callable_receives_declared_array_abi_and_keeps_slice_controls( + slice_by_slice: bool, +) -> None: + stack = np.arange(40, dtype=np.float32).reshape(2, 4, 5) + mask = np.ones_like(stack, dtype=bool) + payload = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with(stack, mask) + seen_shapes = [] + + @numpy + def native_image_operation(image: np.ndarray) -> np.ndarray: + assert isinstance(image, np.ndarray) + seen_shapes.append(image.shape) + return image + 1 + + invocation = compile_function_pattern( + (native_image_operation, {"slice_by_slice": slice_by_slice}), {}, {}, + ).default_group.invocations[0] + result = invocation.runtime_callable( + invocation.main_flow_call_argument(payload), **dict(invocation.runtime_kwargs), + ) + assert seen_shapes == ([(4, 5), (4, 5)] if slice_by_slice else [(2, 4, 5)]) + np.testing.assert_array_equal(result, stack + 1) + assert image_payload_mask(payload) is mask + + +def test_native_flexible_callable_preserves_explicitly_declared_carrier_abi() -> None: + payload = ImagePayloadMetadata().payload_with(np.ones((2, 4, 5)), np.ones((2, 4, 5), dtype=bool)) + + @numpy + def carrier_operation(image: RuntimeArrayPayload) -> RuntimeArrayPayload: + return image + + invocation = compile_function_pattern(carrier_operation, {}, {}).default_group.invocations[0] + assert invocation.main_flow_call_argument(payload) is payload + + @pytest.mark.parametrize("slice_by_slice", (True, False)) def test_flexible_runtime_argument_preserves_plane_context_until_mode_selection( slice_by_slice: bool, From 0d02a574e504daeaab43df27e4a1fcc439f0b25c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 18:36:02 -0400 Subject: [PATCH 2/3] Compose invocation image stacks once in their required memory domain --- openhcs/core/function_patterns.py | 6 +++ plans/runtime_plumbing/IMPLEMENTATION.md | 2 +- tests/unit/test_callable_abi_preparation.py | 56 +++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index 6e3559c946..0681daa915 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -39,6 +39,7 @@ ArtifactSpecCollection, ArtifactSpecRef, ) +from openhcs.core.aligned_image_payload import ImagePayloadStackComposition from openhcs.core.callable_contract import ( CallableContract, FunctionStepExecutionScope, @@ -682,6 +683,11 @@ def convert_input(self, payload: object, source_memory_type: str) -> object: if self._input_memory_type is None: image_payload_data(payload) self.contract.require_memory_types() + if isinstance(payload, ImagePayloadStackComposition): + return payload.compose( + memory_type=self._input_memory_type.value, + device_id=self.input_device_id, + ) source = MemoryType(source_memory_type) if source is self._input_memory_type and not source.is_gpu: mask = image_payload_mask(payload) diff --git a/plans/runtime_plumbing/IMPLEMENTATION.md b/plans/runtime_plumbing/IMPLEMENTATION.md index cddcfb8a2b..bcfbfc66ab 100644 --- a/plans/runtime_plumbing/IMPLEMENTATION.md +++ b/plans/runtime_plumbing/IMPLEMENTATION.md @@ -11,7 +11,7 @@ The active performance objective remains execution and total pipeline latency, w | P2 | RuntimePatternDiscoveryCache; declared source bindings | Implemented in c1650d8. Production consumer probe confirms one physical filename parse across discovery, matching and indexed group selection, and re-admission when parser semantics change. Produced filename aliases remain live. | | P3 | StepOutputManifestStore; CompiledFunctionPattern | Implemented in 88522fb. Ordered address maps update only new records; producer-scoped invalidation eliminates revision counters. Existing immutable DurableSourceMetadata fixes publication slots; source metadata and filename aliases remain live. Publication policy explicitly approved by user. Derived path order is retained without changing insertion order. | | P4 | ProcessingContext; source projection authority; manifest | Implemented in c1650d8. Held authority rebinds strong owners; optional file eligibility and document changes remain live. Lazy logging and retained P3 order remove per-group formatting/sorting. Ambiguity admission still precedes cached payload return. | -| P5 | Compiled invocation; ArrayBridge MemoryType; ImageFileFormat | Core implemented in c1d8728 and 09b353e. Target/device are captured; conversions use the real active source, including skipped calls. Native stacking removes mandatory GPU-host-GPU traffic. Disk format parent owns host projection; memory checkpoints keep their declared framework. #1080 completes native mixed-channel promotion, intensity reconciliation and mask geometry through ArrayBridge's existing operation owner. GTX 1080/CuPy composition receiving forbids downloads and checks exact values, masks, destination devices and independent stacks. Pyclesperanto lacks native changed-shape reshape/broadcast and boolean allocation; those operations report unsupported capability. P5 remains incomplete until a real mixed-device production pipeline has a complete before/after conversion profile, including skipped calls and disk boundaries. No CPU benchmark speedup is inferred from these GPU checks. | +| P5 | Compiled invocation; ArrayBridge MemoryType; ImageFileFormat | Core implemented in c1d8728 and 09b353e. Target/device are captured; conversions use the real active source, including skipped calls. Native stacking removes mandatory GPU-host-GPU traffic. Disk format parent owns host projection; memory checkpoints keep their declared framework. #1080 completes native mixed-channel promotion, intensity reconciliation and mask geometry through ArrayBridge's existing operation owner. GTX 1080/CuPy composition receiving forbids downloads and checks exact values, masks, destination devices and independent stacks. Pyclesperanto lacks native changed-shape reshape/broadcast and boolean allocation; those operations report unsupported capability. The real compiled CPU Gaussian → GPU sharpen → GPU percentile normalization → CPU Gaussian route now completes on saved microscopy inputs, including named mixed-intensity/masked channels, a skipped CPU invocation, a memory checkpoint and a disk roundtrip. #1083 repairs native flexible ABI admission. Invocation composition uses the existing target/device owner once: its canonical bulk conversion census falls from 10 calls / 2,621,440 bytes to 6 calls / 1,638,400 bytes, with exact numeric oracle and masks. The original pre-P5 mixed route fails before completing, so its partial counts are not a complete baseline; the plain grayscale negative control remains unchanged at two transfers. CuCIM is unavailable and installed GPU processors reject 4D colour stacks; promoted-colour full-pipeline receiving remains unqualified. These are conversion counts, not CPU timing or benchmark speedup evidence. | #641 is merged and removes post-compilation kernel warming; normal registry READY is the only child-cache consumer. Full catalog READY qualification passed. #630 (runtime ownership refactor), #635 (intensity maxima), #597 (saved compiler resolution) and #608 (morphology graph intermediates) are merged following the user's explicit request to merge locally qualified work. Their earlier negative whole-pipeline observations remain documented; merge is not evidence of an end-to-end speedup. diff --git a/tests/unit/test_callable_abi_preparation.py b/tests/unit/test_callable_abi_preparation.py index 046995f880..8d2ebef8a1 100644 --- a/tests/unit/test_callable_abi_preparation.py +++ b/tests/unit/test_callable_abi_preparation.py @@ -399,3 +399,59 @@ def declared(*args, **kwargs): RuntimeBatchExecutionDomain.PURE_2D_SLICES: declared, }) assert selected is declared + + +def test_compiled_input_places_named_bundle_once_before_cpu_identity_shortcut(monkeypatch): + from openhcs.core.aligned_image_payload import ( + AlignedImageSliceContext, ImageOutputBundle, + ) + from openhcs.core.function_patterns import compile_function_pattern + from openhcs.core.runtime_image_values import image_payload_mask + from openhcs.processing.backends.processors.numpy_processor import gaussian_blur + + pixels = tuple(np.full((3, 4), value, dtype=np.float32) for value in (0.25, 0.75)) + masks = tuple(np.full((3, 4), value, dtype=bool) for value in (True, False)) + source = ImageOutputBundle( + tuple(ImagePayloadMetadata().payload_with(data, mask) + for data, mask in zip(pixels, masks, strict=True)), + tuple(AlignedImageSliceContext.independent_main_flow(name) + for name in ("DNA", "Membrane")), + ) + calls = [] + original = ImageOutputBundle.compose + + def observed(value, **destination): + calls.append(destination) + return original(value, **destination) + + monkeypatch.setattr(ImageOutputBundle, "compose", observed) + invocation = compile_function_pattern(gaussian_blur, {}, {}).default_group.invocations[0] + placed = invocation.convert_input(source, "numpy") + invocation.main_flow_call_argument(placed) + image_payload_data(placed) + image_payload_mask(placed) + + assert calls == [{"memory_type": "numpy", "device_id": None}] + np.testing.assert_array_equal(image_payload_data(placed), np.stack(pixels)) + assert image_payload_data(placed).shape == (2, 3, 4) + assert image_payload_mask(placed).shape == (2, 3, 4) + assert placed.metadata.source_image_names == ("DNA", "Membrane") + np.testing.assert_array_equal(image_payload_mask(placed), np.stack(masks)) + + +def test_compiled_input_reuses_produced_stack_realization_without_copy(): + from openhcs.core.aligned_image_payload import ( + AlignedImageSliceContext, ProducedImageStack, + ) + from openhcs.core.function_patterns import compile_function_pattern + from openhcs.processing.backends.processors.numpy_processor import gaussian_blur + + source = ProducedImageStack( + tuple(np.full((3, 4), value, dtype=np.float32) for value in (1, 2)), + memory_type="numpy", plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + ) + invocation = compile_function_pattern(gaussian_blur, {}, {}).default_group.invocations[0] + placed = invocation.convert_input(source, "numpy") + assert invocation.convert_input(source, "numpy") is placed + assert image_payload_data(placed) is image_payload_data(source) + assert np.shares_memory(source.slices[0], image_payload_data(placed)) From e1e97f7f2d1b0c886ff91e8cf37a3cc04600c935 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 18:43:27 -0400 Subject: [PATCH 3/3] Record complete mixed-route P0 conversion profile qualification --- plans/runtime_plumbing/IMPLEMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plans/runtime_plumbing/IMPLEMENTATION.md b/plans/runtime_plumbing/IMPLEMENTATION.md index bcfbfc66ab..03a6d85586 100644 --- a/plans/runtime_plumbing/IMPLEMENTATION.md +++ b/plans/runtime_plumbing/IMPLEMENTATION.md @@ -11,7 +11,7 @@ The active performance objective remains execution and total pipeline latency, w | P2 | RuntimePatternDiscoveryCache; declared source bindings | Implemented in c1650d8. Production consumer probe confirms one physical filename parse across discovery, matching and indexed group selection, and re-admission when parser semantics change. Produced filename aliases remain live. | | P3 | StepOutputManifestStore; CompiledFunctionPattern | Implemented in 88522fb. Ordered address maps update only new records; producer-scoped invalidation eliminates revision counters. Existing immutable DurableSourceMetadata fixes publication slots; source metadata and filename aliases remain live. Publication policy explicitly approved by user. Derived path order is retained without changing insertion order. | | P4 | ProcessingContext; source projection authority; manifest | Implemented in c1650d8. Held authority rebinds strong owners; optional file eligibility and document changes remain live. Lazy logging and retained P3 order remove per-group formatting/sorting. Ambiguity admission still precedes cached payload return. | -| P5 | Compiled invocation; ArrayBridge MemoryType; ImageFileFormat | Core implemented in c1d8728 and 09b353e. Target/device are captured; conversions use the real active source, including skipped calls. Native stacking removes mandatory GPU-host-GPU traffic. Disk format parent owns host projection; memory checkpoints keep their declared framework. #1080 completes native mixed-channel promotion, intensity reconciliation and mask geometry through ArrayBridge's existing operation owner. GTX 1080/CuPy composition receiving forbids downloads and checks exact values, masks, destination devices and independent stacks. Pyclesperanto lacks native changed-shape reshape/broadcast and boolean allocation; those operations report unsupported capability. The real compiled CPU Gaussian → GPU sharpen → GPU percentile normalization → CPU Gaussian route now completes on saved microscopy inputs, including named mixed-intensity/masked channels, a skipped CPU invocation, a memory checkpoint and a disk roundtrip. #1083 repairs native flexible ABI admission. Invocation composition uses the existing target/device owner once: its canonical bulk conversion census falls from 10 calls / 2,621,440 bytes to 6 calls / 1,638,400 bytes, with exact numeric oracle and masks. The original pre-P5 mixed route fails before completing, so its partial counts are not a complete baseline; the plain grayscale negative control remains unchanged at two transfers. CuCIM is unavailable and installed GPU processors reject 4D colour stacks; promoted-colour full-pipeline receiving remains unqualified. These are conversion counts, not CPU timing or benchmark speedup evidence. | +| P5 | Compiled invocation; ArrayBridge MemoryType; ImageFileFormat | Core implemented in c1d8728 and 09b353e. Target/device are captured; conversions use the real active source, including skipped calls. Native stacking removes mandatory GPU-host-GPU traffic. Disk format parent owns host projection; memory checkpoints keep their declared framework. #1080 completes native mixed-channel promotion, intensity reconciliation and mask geometry through ArrayBridge's existing operation owner. GTX 1080/CuPy composition receiving forbids downloads and checks exact values, masks, destination devices and independent stacks. Pyclesperanto lacks native changed-shape reshape/broadcast and boolean allocation; those operations report unsupported capability. The real compiled CPU Gaussian → GPU sharpen → GPU percentile normalization → CPU Gaussian route now completes on saved microscopy inputs, including named mixed-intensity/masked channels, a skipped CPU invocation, a memory checkpoint and a disk roundtrip. #1083 repairs native flexible ABI admission. Invocation composition uses the existing target/device owner once: its canonical bulk conversion census falls from 10 calls / 2,621,440 bytes to 6 calls / 1,638,400 bytes, with exact numeric oracle and masks. The original pre-P5 mixed route fails before completing, so its partial counts are not a complete baseline; the plain grayscale negative control remains unchanged at two transfers. CuCIM is unavailable and installed GPU processors reject 4D colour stacks; promoted-colour full-pipeline receiving remains unqualified. The same scratch observer also emits isolated `convert_input` spans through the existing P0 RuntimeProfileLogger on exact ABI baseline 5ed8cf and composition head 53edad: retained `p5-mixed-conversion-profile-{before,after}-v1/runtime-profile.log` and receipts under the 20261006 maintenance root use identical pre-span/post-conversion CUDA synchronization. Both profiles complete the real route with exact oracle, masks, skipped-call events and checkpoint/disk checks. This is one diagnostic pair and a conversion census, not a statistical or CPU/end-to-end benchmark speedup claim. | #641 is merged and removes post-compilation kernel warming; normal registry READY is the only child-cache consumer. Full catalog READY qualification passed. #630 (runtime ownership refactor), #635 (intensity maxima), #597 (saved compiler resolution) and #608 (morphology graph intermediates) are merged following the user's explicit request to merge locally qualified work. Their earlier negative whole-pipeline observations remain documented; merge is not evidence of an end-to-end speedup.