Skip to content

Commit 5859820

Browse files
Merge branch 'main' into feat/expose-stream-metrics-prometheus
2 parents 5e3c4ca + 1cda0fa commit 5859820

File tree

80 files changed

+4795
-247
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+4795
-247
lines changed

docs/workflows/execution_engine_changelog.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,80 @@
22

33
Below you can find the changelog for Execution Engine.
44

5+
## Execution Engine `v1.8.0` | inference `v1.1.1`
6+
7+
!!! Note "Additive change + one breaking change due to bug fix with minimal expected impact"
8+
9+
This release extends the Execution Engine so that steps gated by control flow (e.g. after a
10+
`ContinueIf` block) can run even when they have **no data-derived lineage** — i.e. when they
11+
do not receive batch-oriented inputs from upstream steps. Lineage and execution dimensionality
12+
can now be derived from control flow predecessor steps. Existing workflows are unaffected.
13+
14+
One breaking change introduced is due to the bug fix that affects `Batch.remove_by_indices` with nested batches (see below); impact is
15+
expected to be minimal.
16+
17+
**What changed**
18+
19+
* **Control flow lineage** — The compiler now tracks lineage that comes from control flow steps
20+
(e.g. branches after `ContinueIf`). A new notion of **control flow lineage support** is used when
21+
a step has no batch-oriented data inputs but is preceded by control flow steps: the step’s
22+
execution slices and batch structure are taken from those control flow predecessors.
23+
24+
* **Loosened compatibility check** — Previously, `verify_compatibility_of_input_data_lineage_with_control_flow_lineage`
25+
raised `ControlFlowDefinitionError` for any step that had control flow predecessors but no
26+
data-derived lineage, so such steps could not be compiled. That check is now relaxed: when a step
27+
has no input data lineage, compatibility is not enforced and the step’s lineage is derived from
28+
the control flow predecessor step lineage instead. The strict check still runs when the step
29+
*does* have data-derived lineage, to ensure control flow and data lineage remain compatible.
30+
31+
* **New step patterns** — Steps that are triggered only by control flow and do not consume
32+
batch data now run correctly. For example, you can send email notifications (or run other
33+
side-effect steps) after a `ContinueIf` without wiring any data into parameters like
34+
`message_parameters`; the step will execute once per control flow branch with lineage and
35+
dimensionality taken from the controlling step.
36+
37+
* **`Batch.remove_by_indices` with nested batches (behavioral fix)** — When removing indices
38+
via `Batch.remove_by_indices`, nested `Batch` elements are now recursively filtered by the
39+
same index set. As a result, entries at removed indices (including `None` values) are now
40+
correctly dropped from nested batches as well. Previously, only the top-level batch was
41+
filtered; nested batches were left unchanged.
42+
43+
By default for a `WorkflowBlock`, `accepts_empty_values()`is `False`. While
44+
this was bypassed, blocks consuming such inputs where outright failing as for example `StitchDetectionsBatchBlock`:
45+
46+
```python
47+
def run(
48+
self,
49+
images: Batch[WorkflowImageData],
50+
images_predictions: Batch[Batch[sv.Detections]],
51+
) -> BlockResult:
52+
result = []
53+
for image, image_predictions in zip(images, images_predictions):
54+
image_predictions = [deepcopy(p) for p in image_predictions if len(p)]
55+
for p in image_predictions:
56+
coords = p["parent_coordinates"][0]
57+
...
58+
```
59+
60+
The only core block that this change affects is the `DimensionCollapseBlockV1` block,
61+
As it was wrapping individual inputs in a batch without filtering for None values.
62+
63+
```python
64+
class DimensionCollapseBlockV1(WorkflowBlock):
65+
66+
@classmethod
67+
def get_manifest(cls) -> Type[WorkflowBlockManifest]:
68+
return BlockManifest
69+
70+
def run(self, data: Batch[Any]) -> BlockResult:
71+
return {"output": [e for e in data]}
72+
```
73+
74+
When using the output from this block downstream applications could either outright fail or
75+
silently process None values, unless they filtered those values themselves.
76+
77+
Given that above we reckon the impact will be minimal.
78+
579
## Execution Engine `v1.7.0` | inference `v0.59.0`
680

781
!!! warning "Breaking change regarding step errors in workflows"

inference/core/env.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@
204204

205205
QWEN_3_ENABLED = str2bool(os.getenv("QWEN_3_ENABLED", True))
206206

207+
QWEN_3_5_ENABLED = str2bool(os.getenv("QWEN_3_5_ENABLED", True))
208+
207209
DEPTH_ESTIMATION_ENABLED = str2bool(os.getenv("DEPTH_ESTIMATION_ENABLED", True))
208210

209211
SMOLVLM2_ENABLED = str2bool(os.getenv("SMOLVLM2_ENABLED", True))

inference/core/managers/active_learning.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def register(
7272
) -> None:
7373
try:
7474
resolved_model_id = resolve_roboflow_model_alias(model_id=model_id)
75+
if not hasattr(request, "active_learning_target_dataset"):
76+
return None
7577
target_dataset = (
7678
request.active_learning_target_dataset
7779
or resolved_model_id.split("/")[0]

inference/core/models/inference_models_adapters.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import base64
2+
import io
13
from io import BytesIO
24
from time import perf_counter
35
from typing import Any, List, Optional, Tuple, Union
46

57
import numpy as np
8+
import torch
69
from PIL import Image, ImageDraw, ImageFont
710

811
from inference.core.entities.requests import (
@@ -22,6 +25,8 @@
2225
ObjectDetectionInferenceResponse,
2326
ObjectDetectionPrediction,
2427
Point,
28+
SemanticSegmentationInferenceResponse,
29+
SemanticSegmentationPrediction,
2530
)
2631
from inference.core.env import (
2732
ALLOW_INFERENCE_MODELS_DIRECTLY_ACCESS_LOCAL_PACKAGES,
@@ -48,6 +53,10 @@
4853
MultiLabelClassificationModel,
4954
MultiLabelClassificationPrediction,
5055
ObjectDetectionModel,
56+
SemanticSegmentationModel,
57+
)
58+
from inference_models.models.base.semantic_segmentation import (
59+
SemanticSegmentationResult,
5160
)
5261
from inference_models.models.base.types import PreprocessingMetadata
5362

@@ -855,3 +864,133 @@ def draw_predictions(inference_request, inference_response, class_names: List[st
855864
image = image.convert("RGB")
856865
image.save(buffered, format="JPEG")
857866
return buffered.getvalue()
867+
868+
869+
class InferenceModelsSemanticSegmentationAdapter(Model):
870+
def __init__(self, model_id: str, api_key: str = None, **kwargs):
871+
super().__init__()
872+
873+
self.metrics = {"num_inferences": 0, "avg_inference_time": 0.0}
874+
875+
self.api_key = api_key if api_key else API_KEY
876+
model_id = resolve_roboflow_model_alias(model_id=model_id)
877+
878+
self.task_type = "semantic-segmentation"
879+
880+
extra_weights_provider_headers = get_extra_weights_provider_headers(
881+
countinference=kwargs.get("countinference"),
882+
service_secret=kwargs.get("service_secret"),
883+
)
884+
backend = list(
885+
VALID_INFERENCE_MODELS_BACKENDS.difference(
886+
DISABLED_INFERENCE_MODELS_BACKENDS
887+
)
888+
)
889+
self._model: SemanticSegmentationModel = AutoModel.from_pretrained(
890+
model_id_or_path=model_id,
891+
api_key=self.api_key,
892+
allow_untrusted_packages=ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES,
893+
allow_direct_local_storage_loading=ALLOW_INFERENCE_MODELS_DIRECTLY_ACCESS_LOCAL_PACKAGES,
894+
weights_provider_extra_headers=extra_weights_provider_headers,
895+
backend=backend,
896+
**kwargs,
897+
)
898+
self.class_names = list(self._model.class_names)
899+
900+
@property
901+
def class_map(self):
902+
# match segment.roboflow.com
903+
return {str(k): v for k, v in enumerate(self.class_names)}
904+
905+
def map_inference_kwargs(self, kwargs: dict) -> dict:
906+
return kwargs
907+
908+
def preprocess(self, image: Any, **kwargs):
909+
is_batch = isinstance(image, list)
910+
images = image if is_batch else [image]
911+
np_images: List[np.ndarray] = [
912+
load_image_bgr(
913+
v,
914+
disable_preproc_auto_orient=kwargs.get(
915+
"disable_preproc_auto_orient", False
916+
),
917+
)
918+
for v in images
919+
]
920+
mapped_kwargs = self.map_inference_kwargs(kwargs)
921+
return self._model.pre_process(np_images, **mapped_kwargs)
922+
923+
def predict(self, img_in, **kwargs):
924+
mapped_kwargs = self.map_inference_kwargs(kwargs)
925+
return self._model.forward(img_in, **mapped_kwargs)
926+
927+
def postprocess(
928+
self,
929+
predictions: torch.Tensor,
930+
preprocess_return_metadata: PreprocessingMetadata,
931+
**kwargs,
932+
) -> List[SemanticSegmentationInferenceResponse]:
933+
mapped_kwargs = self.map_inference_kwargs(kwargs)
934+
segmentation_results = self._model.post_process(
935+
predictions, preprocess_return_metadata, **mapped_kwargs
936+
)
937+
938+
responses: List[SemanticSegmentationInferenceResponse] = []
939+
for preproc_metadata, segmentation in zip(
940+
preprocess_return_metadata, segmentation_results
941+
):
942+
height = preproc_metadata.original_size.height
943+
width = preproc_metadata.original_size.width
944+
response_image = InferenceResponseImage(width=width, height=height)
945+
# WARNING! This way of conversion is hazardous - first of all, if background class is not in class names,
946+
# for certain pre-processing, we end up with -1 values which will be wrapped to 255 - second of all,
947+
# we can support only 256 classes - those constraints should be fine until inference 2.0
948+
response_predictions = SemanticSegmentationPrediction(
949+
segmentation_mask=self.img_to_b64_str(
950+
segmentation.segmentation_map.to(torch.uint8)
951+
),
952+
confidence_mask=self.img_to_b64_str(
953+
(segmentation.confidence * 255).to(torch.uint8)
954+
),
955+
class_map=self.class_map,
956+
image=dict(response_image),
957+
)
958+
response = SemanticSegmentationInferenceResponse(
959+
predictions=response_predictions,
960+
image=response_image,
961+
)
962+
responses.append(response)
963+
return responses
964+
965+
def clear_cache(self, delete_from_disk: bool = True) -> None:
966+
"""Clears any cache if necessary. TODO: Implement this to delete the cache from the experimental model.
967+
968+
Args:
969+
delete_from_disk (bool, optional): Whether to delete cached files from disk. Defaults to True.
970+
"""
971+
pass
972+
973+
def img_to_b64_str(self, img: torch.Tensor) -> str:
974+
if img.dtype != torch.uint8:
975+
raise ValueError(
976+
f"img_to_b64_str requires uint8 tensor but got dtype {img.dtype}"
977+
)
978+
979+
img = Image.fromarray(img.cpu().numpy())
980+
buffered = io.BytesIO()
981+
img.save(buffered, format="PNG")
982+
983+
img_str = base64.b64encode(buffered.getvalue())
984+
img_str = img_str.decode("ascii")
985+
986+
return img_str
987+
988+
def draw_predictions(
989+
self,
990+
inference_request: InferenceRequest,
991+
inference_response: InferenceResponse,
992+
) -> bytes:
993+
raise NotImplementedError(
994+
"draw_predictions(...) is not implemented for semantic segmentation models - responses contain "
995+
"visualization already."
996+
)

inference/core/models/semantic_segmentation_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def img_to_b64_str(self, img: torch.Tensor) -> str:
134134
f"img_to_b64_str requires uint8 tensor but got dtype {img.dtype}"
135135
)
136136

137-
img = Image.fromarray(img.numpy())
137+
img = Image.fromarray(img.cpu().numpy())
138138
buffered = io.BytesIO()
139139
img.save(buffered, format="PNG")
140140

inference/core/registries/roboflow.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
MODELS_CACHE_AUTH_CACHE_MAX_SIZE,
2020
MODELS_CACHE_AUTH_CACHE_TTL,
2121
MODELS_CACHE_AUTH_ENABLED,
22+
USE_INFERENCE_MODELS,
2223
)
2324
from inference.core.exceptions import (
2425
MissingApiKeyError,
@@ -34,6 +35,7 @@
3435
MODEL_TYPE_KEY,
3536
PROJECT_TASK_TYPE_KEY,
3637
ModelEndpointType,
38+
get_model_metadata_from_inference_models_registry,
3739
get_roboflow_dataset_type,
3840
get_roboflow_instant_model_data,
3941
get_roboflow_model_data,
@@ -129,13 +131,20 @@ def _check_if_api_key_has_access_to_model(
129131
countinference=countinference,
130132
service_secret=service_secret,
131133
)
132-
else:
134+
elif not USE_INFERENCE_MODELS:
133135
get_roboflow_instant_model_data(
134136
api_key=api_key,
135137
model_id=model_id,
136138
countinference=countinference,
137139
service_secret=service_secret,
138140
)
141+
else:
142+
get_model_metadata_from_inference_models_registry(
143+
api_key=api_key,
144+
model_id=model_id,
145+
countinference=countinference,
146+
service_secret=service_secret,
147+
)
139148
except RoboflowAPINotAuthorizedError:
140149
return False
141150
return True
@@ -220,14 +229,22 @@ def get_model_type(
220229
device_id=GLOBAL_DEVICE_ID,
221230
).get("ort")
222231
project_task_type = api_data.get("type", "object-detection")
223-
else:
232+
elif not USE_INFERENCE_MODELS:
224233
api_data = get_roboflow_instant_model_data(
225234
api_key=api_key,
226235
model_id=model_id,
227236
countinference=countinference,
228237
service_secret=service_secret,
229238
)
230239
project_task_type = api_data.get("taskType", "object-detection")
240+
else:
241+
api_data = get_model_metadata_from_inference_models_registry(
242+
api_key=api_key,
243+
model_id=model_id,
244+
countinference=countinference,
245+
service_secret=service_secret,
246+
)
247+
project_task_type = api_data.get("taskType", "object-detection")
231248
if api_data is None:
232249
raise ModelArtefactError("Error loading model artifacts from Roboflow API.")
233250

0 commit comments

Comments
 (0)