Skip to content

Commit 831583a

Browse files
Clarify docs and fix issue with input parameters not being broadcast properly
1 parent 714d88c commit 831583a

File tree

12 files changed

+228
-20
lines changed

12 files changed

+228
-20
lines changed

docs/workflows/create_workflow_block.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,7 @@ the method signatures.
15281528
In this example, the block visualises crops predictions and creates tiles
15291529
presenting all crops predictions in single output image.
15301530

1531-
```{ .py linenums="1" hl_lines="29-31 48-49 59-60"}
1531+
```{ .py linenums="1" hl_lines="30-32 34-36 53-55 65-66"}
15321532
from typing import List, Literal, Type, Union
15331533

15341534
import supervision as sv
@@ -1556,10 +1556,15 @@ the method signatures.
15561556
crops_predictions: Selector(
15571557
kind=[OBJECT_DETECTION_PREDICTION_KIND]
15581558
)
1559+
scalar_parameter: Union[float, Selector()]
15591560
15601561
@classmethod
15611562
def get_output_dimensionality_offset(cls) -> int:
15621563
return -1
1564+
1565+
@classmethod
1566+
def get_parameters_enforcing_auto_batch_casting(cls) -> List[str]:
1567+
return ["crops", "crops_predictions"]
15631568
15641569
@classmethod
15651570
def describe_outputs(cls) -> List[OutputDefinition]:
@@ -1578,6 +1583,7 @@ the method signatures.
15781583
self,
15791584
crops: Batch[WorkflowImageData],
15801585
crops_predictions: Batch[sv.Detections],
1586+
scalar_parameter: float,
15811587
) -> BlockResult:
15821588
annotator = sv.BoxAnnotator()
15831589
visualisations = []
@@ -1591,18 +1597,22 @@ the method signatures.
15911597
return {"visualisations": tile}
15921598
```
15931599

1594-
* in lines `29-31` manifest class declares output dimensionality
1600+
* in lines `30-32` manifest class declares output dimensionality
15951601
offset - value `-1` should be understood as decreasing dimensionality level by `1`
15961602

1597-
* in lines `48-49` you can see the impact of output dimensionality decrease
1598-
on the method signature. Both inputs are artificially wrapped in `Batch[]` container.
1599-
This is done by Execution Engine automatically on output dimensionality decrease when
1600-
all inputs have the same dimensionality to enable access to all elements occupying
1601-
the last dimensionality level. Obviously, only elements related to the same element
1603+
* in lines `34-36` manifest class declares `run(...)` method inputs that will be subject to auto-batch casting
1604+
ensuring that the signature is always stable. Auto-batch casting was introduced in Execution Engine `v0.1.6.0`
1605+
- refer to [changelog](./execution_engine_changelog.md) for more details.
1606+
1607+
* in lines `53-55` you can see the impact of output dimensionality decrease
1608+
on the method signature. First two inputs (declared in line `36`) are artificially wrapped in `Batch[]`
1609+
container, whereas `scalar_parameter` remains primitive type. This is done by Execution Engine automatically
1610+
on output dimensionality decrease when all inputs have the same dimensionality to enable access to
1611+
all elements occupying the last dimensionality level. Obviously, only elements related to the same element
16021612
from top-level batch will be grouped. For instance, if you had two input images that you
16031613
cropped - crops from those two different images will be grouped separately.
16041614

1605-
* lines `59-60` illustrate how output is constructed - single value is returned and that value
1615+
* lines `65-66` illustrate how output is constructed - single value is returned and that value
16061616
will be indexed by Execution Engine in output batch with reduced dimensionality
16071617

16081618
=== "different input dimensionalities"

docs/workflows/execution_engine_changelog.md

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ exception being output dimensionality changes introduced by the block itself. As
5656

5757
* **collapse batches into scalars** (when the block decreases dimensionality).
5858

59-
* The only potential friction point arises **when a block that does not accept batches** (and thus does not denote
59+
* The two potential friction point arises - first **when a block that does not accept batches** (and thus does not denote
6060
batch-accepting inputs) **decreases output dimensionality**. In previous versions, the Execution Engine handled this by
6161
applying dimensionality wrapping: all batch-oriented inputs were wrapped with an additional `Batch[T]` dimension,
6262
allowing the block’s `run(...)` method to perform reduce operations across the list dimension. With Auto Batch Casting,
@@ -65,6 +65,22 @@ scalars or batches, making casting nondeterministic. To address this, a new mani
6565
`get_parameters_enforcing_auto_batch_casting(...)`. This method must return the list of parameters for which batch
6666
casting should be enforced when dimensionality is decreased. It is not expected to be used in any other context.
6767

68+
!!! warning "Impact of new method on existing blocks"
69+
70+
The requirement of defining `get_parameters_enforcing_auto_batch_casting(...)` method to fully use
71+
Auto Batch Casting feature in the case described above is non-strict. If the block will not be changed,
72+
the only effect will be that workflows wchich were **previously failing** with compilation error may
73+
work or fail with **runtime error**, dependent on the details of block `run(...)` method implementation.
74+
75+
* The second friction point arises when there is a block declaring input fields supporting batches and scalars using
76+
`get_parameters_accepting_batches_and_scalars(...)` - by default, Execution Engine will skip auto-casting for such
77+
parameters, as the method was historically **always a way to declare that block itself has ability to broadcast scalars
78+
into batches** - see
79+
[implementation of `roboflow_core/detections_transformation@v1`](/inference/core/workflows/core_steps/transformations/detections_transformation/v1.py)
80+
block. In a way, Auto Batch Casting is *redundant* for those blocks - so we propose leaving them as is and
81+
upgrade to use `get_parameters_enforcing_auto_batch_casting(...)` instead of
82+
`get_parameters_accepting_batches_and_scalars(...)` in new versions of such blocks.
83+
6884
* In earlier versions, a hard constraint existed: dimensionality collapse could only occur at levels ≥ 2 (i.e. only
6985
on nested batches). This limitation is now removed. Dimensionality collapse blocks may also operate on scalars, with
7086
the output dimensionality “bouncing off” the zero ground.
@@ -86,7 +102,88 @@ situation is known limitation of Workflows Compiler.
86102
Contact Roboflow team through github issues (https://github.com/roboflow/inference/issues)
87103
providing full context of the problem - including workflow definition you use.
88104
```
105+
### Migration guide
106+
107+
??? Hint "Adding `get_parameters_enforcing_auto_batch_casting(...)` method"
108+
109+
Blocks which decrease output dimensionality and do not define batch-oriented inputs needs to
110+
declare all inputs which implementation expects to have wrapped in `Batch[T]` with the new class
111+
method of block manifest called `get_parameters_enforcing_auto_batch_casting(...)`
112+
113+
```{ .py linenums="1" hl_lines="34-36 53-54"}
114+
from typing import List, Literal, Type, Union
115+
116+
import supervision as sv
117+
118+
from inference.core.workflows.execution_engine.entities.base import (
119+
Batch,
120+
OutputDefinition,
121+
WorkflowImageData,
122+
)
123+
from inference.core.workflows.execution_engine.entities.types import (
124+
IMAGE_KIND,
125+
OBJECT_DETECTION_PREDICTION_KIND,
126+
Selector,
127+
)
128+
from inference.core.workflows.prototypes.block import (
129+
BlockResult,
130+
WorkflowBlock,
131+
WorkflowBlockManifest,
132+
)
133+
134+
135+
class BlockManifest(WorkflowBlockManifest):
136+
type: Literal["my_plugin/tile_detections@v1"]
137+
crops: Selector(kind=[IMAGE_KIND])
138+
crops_predictions: Selector(
139+
kind=[OBJECT_DETECTION_PREDICTION_KIND]
140+
)
141+
scalar_parameter: Union[float, Selector()]
142+
143+
@classmethod
144+
def get_output_dimensionality_offset(cls) -> int:
145+
return -1
146+
147+
@classmethod
148+
def get_parameters_enforcing_auto_batch_casting(cls) -> List[str]:
149+
return ["crops", "crops_predictions"]
150+
151+
@classmethod
152+
def describe_outputs(cls) -> List[OutputDefinition]:
153+
return [
154+
OutputDefinition(name="visualisations", kind=[IMAGE_KIND]),
155+
]
156+
157+
158+
class TileDetectionsBlock(WorkflowBlock):
159+
160+
@classmethod
161+
def get_manifest(cls) -> Type[WorkflowBlockManifest]:
162+
return BlockManifest
163+
164+
def run(
165+
self,
166+
crops: Batch[WorkflowImageData],
167+
crops_predictions: Batch[sv.Detections],
168+
scalar_parameter: float,
169+
) -> BlockResult:
170+
print("This is parameter which will not be auto-batch cast!", scalar_parameter)
171+
annotator = sv.BoxAnnotator()
172+
visualisations = []
173+
for image, prediction in zip(crops, crops_predictions):
174+
annotated_image = annotator.annotate(
175+
image.numpy_image.copy(),
176+
prediction,
177+
)
178+
visualisations.append(annotated_image)
179+
tile = sv.create_tiles(visualisations)
180+
return {"visualisations": tile}
181+
```
182+
183+
* in lines `34-36` one needs to add declaration of fields that will be subject to enforced auto-batch casting
89184

185+
* as a result of the above, input parameters of run method (lines `53-54`) will be wrapped into `Batch[T]` by
186+
Execution Engine.
90187

91188
## Execution Engine `v1.5.0` | inference `v0.38.0`
92189

docs/workflows/workflow_execution.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ influencing the processing for all elements in the batch and this type of data w
124124
the reference images remain unchanged as you process each input. Thus, the reference images are considered
125125
*scalar* data, while the list of input images is *batch-oriented*.
126126

127+
**Great news!**
128+
129+
Since Execution Engine `v1.6.0`, the practical aspects of dealing with *scalars* and *batches* are offloaded to
130+
the Execution Engine (refer to [changelog](./execution_engine_changelog.md) for more details). As a block
131+
developer, it is still important to understand the difference, but when building blocks you are not forced to
132+
think about the nuances that much.
133+
134+
127135
To illustrate the distinction, Workflow definitions hold inputs of the two categories:
128136

129137
- **Scalar inputs** - like `WorkflowParameter`

docs/workflows/workflows_execution_engine.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,35 @@ batch-oriented input, it will be treated as a SIMD step.
8686
Non-SIMD steps, by contrast, are expected to deliver a single result for the input data. In the case of non-SIMD
8787
flow-control steps, they affect all downstream steps as a whole, rather than individually for each element in a batch.
8888

89+
Historically, Execution Engine could not handle well al scenarios when non-SIMD steps' outputs were fed into SIMD steps
90+
inputs - causing compilation error due to lack of ability to automatically cast such outputs into batches when feeding
91+
into SIMD seps. Starting with Execution Engine `v1.6.0`, the handling of SIMD and non-SIMD blocks has been improved
92+
through the introduction of **Auto Batch Casting**:
93+
94+
* When a SIMD input is detected but receives scalar data, the Execution Engine automatically casts it into a batch.
95+
96+
* The dimensionality of the batch is determined at compile time, using *lineage* information from other
97+
batch-oriented inputs when available. Missing dimensions are generated in a manner similar to `torch.unsqueeze(...)`.
98+
99+
* Outputs are evaluated against the casting context - leaving them as scalars when block keeps or decreases output
100+
dimensionality or **creating new batches** when increase of dimensionality is expected.
101+
102+
!!! warning "We don't support multiple sources of batch-oriented data"
103+
104+
While Auto Batch Casting simplifies mixing SIMD and non-SIMD blocks, there is one major limitation to be aware of.
105+
106+
If multiple first-level batches are created from different origins (for instance inputs and steps taking scalars
107+
and raising output dimensionality into batch at first level of depth), the Execution Engine cannot deterministically
108+
construct the output. In previous versions, the assumption was that **outputs were lists directly tied to inputs
109+
batch order**. With Auto Batch Casting, batches may also be generated dynamically, and no deterministic ordering
110+
can be guaranteed (imagine scenario when you feed batch of 4 images, and there is a block generating dynamic batch
111+
with 3 images - when results are to be returned, Execution Engine is unable to determine a single input batch which
112+
would dictate output order alignment, which is a hard requirement caused by falty design choices).
113+
114+
To prevent unpredictable behaviour, the Execution Engine asserts in this scenario and raises an error instead of
115+
proceeding. Resolving this design flaw requires breaking changes and is therefore deferred to
116+
**Execution Engine v2.0.**
117+
89118

90119
### Preparing step inputs
91120

inference/core/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.52.1"
1+
__version__ = "0.53.0"
22

33

44
if __name__ == "__main__":

inference/core/workflows/core_steps/fusion/dimension_collapse/v1.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ def get_output_dimensionality_offset(
5959
) -> int:
6060
return -1
6161

62+
@classmethod
63+
def get_parameters_enforcing_auto_batch_casting(cls) -> List[str]:
64+
return ["data"]
65+
6266
@classmethod
6367
def describe_outputs(cls) -> List[OutputDefinition]:
6468
return [

inference/core/workflows/execution_engine/introspection/schema_parser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,10 @@ def retrieve_selectors_from_simple_property(
334334
)
335335
if declared_points_to_batch == "dynamic":
336336
if property_name in inputs_accepting_batches_and_scalars:
337-
points_to_batch = {True, False}
337+
if property_name in inputs_enforcing_auto_batch_casting:
338+
points_to_batch = {True}
339+
else:
340+
points_to_batch = {True, False}
338341
else:
339342
points_to_batch = {
340343
property_name in inputs_accepting_batches

inference/core/workflows/execution_engine/v1/compiler/graph_constructor.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,18 +1577,31 @@ def verify_declared_batch_compatibility_against_actual_inputs(
15771577
batch_compatibility_of_properties: Dict[str, Set[bool]],
15781578
) -> Set[str]:
15791579
scalar_parameters_to_be_batched = set()
1580+
parameters_accepting_batches_and_scalars = set(
1581+
step_node_data.step_manifest.get_parameters_accepting_batches_and_scalars()
1582+
)
1583+
hardcoded_inputs_to_be_batch_compatible = set(
1584+
step_node_data.step_manifest.get_parameters_enforcing_auto_batch_casting()
1585+
+ step_node_data.step_manifest.get_parameters_accepting_batches()
1586+
)
15801587
for property_name, input_definition in input_data.items():
15811588
if property_name not in batch_compatibility_of_properties:
1582-
# only values plugged via selectors are to be validated
1583-
continue
1584-
if input_definition.is_compound_input():
1589+
actual_input_is_batch = {False}
1590+
if property_name in parameters_accepting_batches_and_scalars:
1591+
batch_compatibility = {True, False}
1592+
elif property_name in hardcoded_inputs_to_be_batch_compatible:
1593+
batch_compatibility = {True}
1594+
else:
1595+
continue
1596+
elif input_definition.is_compound_input():
15851597
actual_input_is_batch = {
15861598
element.is_batch_oriented()
15871599
for element in input_definition.iterate_through_definitions()
15881600
}
1601+
batch_compatibility = batch_compatibility_of_properties[property_name]
15891602
else:
15901603
actual_input_is_batch = {input_definition.is_batch_oriented()}
1591-
batch_compatibility = batch_compatibility_of_properties[property_name]
1604+
batch_compatibility = batch_compatibility_of_properties[property_name]
15921605
step_accepts_batch_input = step_node_data.step_manifest.accepts_batch_input()
15931606
if (
15941607
step_accepts_batch_input

inference/core/workflows/execution_engine/v1/dynamic_blocks/block_assembler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,9 @@ def assembly_manifest_class_methods(
381381
"get_parameters_accepting_batches_and_scalars",
382382
classmethod(get_parameters_accepting_batches_and_scalars),
383383
)
384-
get_parameters_enforcing_auto_batch_casting = lambda cls: list()
384+
get_parameters_enforcing_auto_batch_casting = (
385+
lambda cls: manifest_description.get_parameters_enforcing_auto_batch_casting
386+
)
385387
setattr(
386388
manifest_class,
387389
"get_parameters_enforcing_auto_batch_casting",

inference/core/workflows/execution_engine/v1/dynamic_blocks/entities.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ class ManifestDescription(BaseModel):
116116
"Value will override `accepts_batch_input` if non-empty "
117117
"list is provided, `accepts_batch_input` is kept not to break backward compatibility.",
118118
)
119+
get_parameters_enforcing_auto_batch_casting: List[str] = Field(
120+
default_factory=list,
121+
description="List of parameters, for which auto-batch casting should be enforced, making sure that the block "
122+
"run(...) method will always receive the parameters as batches, not scalars. This property is important for "
123+
"blocks decreasing output dimensionality which do not define neither `batch_oriented_parameters` nor "
124+
"`parameters_with_scalars_and_batches`.",
125+
)
119126

120127

121128
class PythonCode(BaseModel):

0 commit comments

Comments
 (0)