From 75fa16ab024ff517e8e13c865ee8f5f2c38ee8dc Mon Sep 17 00:00:00 2001 From: ff225 Date: Tue, 14 Jul 2026 08:39:34 +0200 Subject: [PATCH] Fix hardcoded model lookup and fragile parsing in split_inference The /api/split_inference endpoint (issue #47) duplicated a hardcoded {size: folder_name} mapping instead of reusing settings.yaml's "model" section, and derived the device size with a fragile device_id.split("_")[1].split("x")[0] that raised an unhandled IndexError on malformed input. - Resolve model_dir via settings.yaml's "model" section (same source RequestHandler/ModelManager use), removing the duplicated dict. - Parse device size with a validating regex, returning a clean 400 on malformed device_id instead of an unhandled exception. - Leave the skip_connections dict in place with a comment: it encodes the FOMO submodel's fixed network topology, not deployment config, so migrating it into settings.yaml wouldn't add value. Full integration with RequestHandler/OffloadingAlgo/ModelManager's request flow was considered and scoped out: this endpoint's synchronous request/response shape (decode tensor, run remaining layers, return prediction) doesn't match the main pipeline's async device-registration and telemetry flow, so folding it in would be a disproportionate rewrite for what's still a validation/prototype endpoint. It keeps tests/test_accuracy/test_ImageWithBox.py's split-vs-local accuracy comparison working across all 5 FOMO sizes. Adds tests/integration/test_split_inference_validation.py, a fast, artifact-free test covering the new validation paths (malformed device_id, unknown model size), so this endpoint has coverage in normal CI beyond the artifact-gated accuracy suite. --- src/server/communication/http_server.py | 76 ++++++++++++++----- .../test_split_inference_validation.py | 71 +++++++++++++++++ 2 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 tests/integration/test_split_inference_validation.py diff --git a/src/server/communication/http_server.py b/src/server/communication/http_server.py index 25c1831..a0c3dc2 100644 --- a/src/server/communication/http_server.py +++ b/src/server/communication/http_server.py @@ -3,9 +3,11 @@ from fastapi import FastAPI, HTTPException, Request from starlette.middleware.gzip import GZipMiddleware from server.communication.inference_protocol import InvalidInferencePayload +from server.commons import ModelFiles from server.logger.log import logger import asyncio import ntplib +import re import threading import time from pathlib import Path @@ -298,16 +300,13 @@ async def close_simulation_csv(): }, ) - SERVER_MODELS_DIR = Path("src/server/models/test") - - @self.app.post("/api/split_inference") async def split_inference(request: Request): try: import tensorflow as tf body = await request.body() - + # --- 1. PARSING DEL PAYLOAD BINARIO --- offset = 0 timestamp = struct.unpack_from("x'." + ) + }, + ) + + nome_cartella = self._resolve_model_dir(size) if not nome_cartella: return JSONResponse(status_code=400, content={"error": f"Modello per size {size} non trovato."}) - - tflite_dir = Path("src/server/models/test") / nome_cartella / "layers" / "tflite" + + tflite_dir = Path(ModelFiles.model_save_path) / "test" / nome_cartella / "layers" / "tflite" tflite_files = sorted( tflite_dir.glob("*.tflite"), @@ -363,7 +369,11 @@ async def split_inference(request: Request): input_data = np.frombuffer(tensor_bytes, dtype=prev_out["dtype"]).copy() input_data = input_data.reshape(tuple(prev_out["shape"])) - # --- 4. COMPLETAMENTO INFERENZA (layer rimanenti) --- + # --- 4. COMPLETAMENTO INFERENZA (layer rimanenti) --- + # These indices are inherent to the FOMO submodel's network + # topology (fixed at export time), not a runtime/deployment + # setting -- so they intentionally stay a local constant rather + # than moving into settings.yaml alongside model_dir. skip_connections = {25: [16, 24], 43: [34, 42], 52: [43, 51]} # Inizializziamo la memoria del server con l'ultimo output noto @@ -461,6 +471,34 @@ async def split_inference(request: Request): }, ) + _DEVICE_SIZE_RE = re.compile(r"(\d+)x\1(?:$|[^\d])") + + @classmethod + def _parse_device_size(cls, device_id: str) -> str | None: + """Extract the square input size (e.g. "48") from a device_id. + + Expects a `..._x...` pattern (e.g. "device_48x48"). Returns + None instead of raising when the id doesn't match, so callers can + surface a clean 400 response rather than an unhandled IndexError from + the previous `device_id.split("_")[1].split("x")[0]` parsing. + """ + match = cls._DEVICE_SIZE_RE.search(device_id) + return match.group(1) if match else None + + def _resolve_model_dir(self, size: str) -> str | None: + """Look up the model folder name for a given size from settings.yaml. + + Uses the same "model" section (keyed by e.g. "fomo_48x48" -> + {"model_dir": "FOMO_48_CUT", ...}) that RequestHandler/ModelManager use + for the main pipeline, instead of a hardcoded {size: folder} dict. + """ + models_config = self._settings().get("model", {}) + model_key = f"fomo_{size}x{size}" + model_cfg = models_config.get(model_key) + if not model_cfg: + return None + return model_cfg.get("model_dir") + def _load_verbose_config(self): """Load verbose configuration from cached settings.""" return self._settings().get("verbose", False) diff --git a/tests/integration/test_split_inference_validation.py b/tests/integration/test_split_inference_validation.py new file mode 100644 index 0000000..72491df --- /dev/null +++ b/tests/integration/test_split_inference_validation.py @@ -0,0 +1,71 @@ +"""Fast, artifact-free coverage for /api/split_inference's validation paths. + +These tests exercise only the request parsing / model-folder lookup logic +(bad device_id, unknown size) and do not require TFLite model artifacts or a +running external server, so they run in normal CI. The accuracy-focused, +artifact-backed exercise of this endpoint lives in +tests/test_accuracy/test_ImageWithBox.py, gated behind +SCIOT_RUN_ACCURACY_TESTS=1. +""" + +import struct + +import pytest + +from server.communication.http_server import HttpServer + + +def _build_split_payload(device_id: str) -> bytes: + """Build a minimal (but well-formed) split_inference binary payload.""" + device_id_bytes = device_id.encode("utf-8") + tensor_bytes = b"\x00" * 4 # single float32 element, contents irrelevant + return struct.pack( + f"