From ecbb1a0113851bd0affba89fc6079662927f6da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E4=BD=B3=E6=B5=A9Alben?= <35291372+XiaoBinGan@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:14:18 +0800 Subject: [PATCH] Fix #4658: the picture reader follows an engine update when the old folder is still there with_current_reader() replaced a run config's vision.exe only when the absolute path was gone. `strata update` keeps the old engine folder, so the old reader still existed and the server kept starting it: the picture reader never followed the engine to 0.1.54, with nothing warning about it. A reader that sits in another engine package folder is now replaced as well, and the same holds for cpu_exe. A reader outside components/engine/ is the person's own file and is left exactly as it is, and the missing-path case behaves as before. --- serve/server.py | 45 +++++++++++++++++++++++++++++--------------- serve/test_server.py | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/serve/server.py b/serve/server.py index 768f6bcc374..3dc296bf737 100644 --- a/serve/server.py +++ b/serve/server.py @@ -8864,32 +8864,47 @@ def default_model_name(cfg: dict) -> str: return cfg.get("model_name") or FAMILY_MODEL_NAMES.get(cfg.get("arch") or "qwen4exp", "qwen3.8-flash-next") +def _in_another_engine_package(path: str, package: Path) -> bool: + """True when `path` sits in an engine package folder that is not `package` - the old folder an engine update leaves + behind. Only the components/engine/ layout counts, so a reader the person put somewhere else is left alone.""" + parts = [part.lower() for part in Path(path).parent.parts] + if "components" not in parts or "engine" not in parts: + return False + try: + return Path(path).parent != package + except (OSError, ValueError): + return False + + def with_current_reader(cfg: dict, package: Path) -> dict: - """The picture reader (vision "exe", "cpu_exe") is written into the run config as an absolute path inside the engine package's folder, - and that folder is named after the engine version: after an engine update, or in a container made for another card, the path is gone - and the server would stop with "server_exited". Use the same file of the package that is installed now (the reader ships in the package).""" + """The picture reader (vision "exe", "cpu_exe") is written into the run config as an absolute path inside the engine package's + folder, and that folder is named after the engine version: after an engine update, or in a container made for another card, + the path is gone and the server would stop with "server_exited". Use the same file of the package that is installed now (the + reader ships in the package). + + A missing path is not the only stale case (#4658): `strata update` keeps the old engine folder, so the old reader is still + there and the server went on starting it after an update. A reader in another engine package folder is replaced as well; a + reader outside components/engine/ is the person's own file and is left as it is.""" vision = cfg.get("vision") if not isinstance(vision, dict): return cfg fixed = dict(vision) for key in ("exe", "cpu_exe"): old = fixed.get(key) - if isinstance(old, str) and old and not engine_component.is_reference(old) and os.path.isabs(old): - new = package / old.replace("\\", "/").rsplit("/", 1)[-1] - gone = not os.path.exists(old) - # #4658: the old engine folder is kept after `strata update`, so the old reader still exists; one in a SIBLING engine folder - # (components/engine/-...) is the engine's reader from before the update, and the engine in use has its own - older = (not gone and Path(old).parent != package and Path(old).parent.parent == package.parent - and package.parent.name.lower() == "engine") - if (gone or older) and new.is_file(): - print(f"[strata] the picture reader {old} " + ("is not there any more" if gone else "belongs to an older engine") - + f"; using {new} from the engine package in use", flush=True) - fixed[key] = str(new) + if not isinstance(old, str) or not old or engine_component.is_reference(old) or not os.path.isabs(old): + continue + gone = not os.path.exists(old) + if not gone and not _in_another_engine_package(old, package): + continue + new = package / old.replace("\\", "/").rsplit("/", 1)[-1] + if new.is_file(): + why = "is not there any more" if gone else "belongs to an engine that is no longer the one in use" + print(f"[strata] the picture reader {old} {why}; using {new} from the engine package in use", flush=True) + fixed[key] = str(new) if fixed == vision: return cfg return {**cfg, "vision": fixed} - def with_component_engine(cfg: dict) -> dict: """A run config that names "component:strata-engine" gets the engine package for this PC: the path in `exe`, and the package's own folder first on the library path (the GPU runtime libraries ship beside the binary).""" diff --git a/serve/test_server.py b/serve/test_server.py index 251f1837664..5fa5712aaed 100644 --- a/serve/test_server.py +++ b/serve/test_server.py @@ -21,6 +21,7 @@ from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from serve import server # noqa: E402 with_current_reader tests (#4658) from serve.frontend import ChatTemplate, literal_tags, mark_think_literals, unmark_think_literals # noqa: E402 from serve.server import (ANSWER_RESERVE_MIN, CTX_SLACK, ByteTokenizer, EngineDied, EngineNonFinite, GpuBusy, MockEngine, # noqa: E402 PP_DONE_TAIL, Service, @@ -6311,6 +6312,48 @@ def test_a_vision_answer_in_time_is_unchanged(self): self.assertEqual(v._readline(5.0, "x"), "OK 7 1 1 1\n") +class CurrentReader(unittest.TestCase): + """#4658: `strata update` keeps the old engine folder, so a missing reader was not the only stale case.""" + + def _package(self, root, version): + package = Path(root) / "components" / "engine" / version + package.mkdir(parents=True, exist_ok=True) + (package / "strata-vision.exe").write_bytes(b"reader") + return package + + def test_a_reader_in_an_old_engine_package_is_replaced_even_when_it_still_exists(self): + with tempfile.TemporaryDirectory() as tmp: + old = self._package(tmp, "0.1.52-windows-x86_64-cuda-cuda13") + new = self._package(tmp, "0.1.54-windows-x86_64-cuda-cuda13") + cfg = {"vision": {"exe": str(old / "strata-vision.exe"), "mmproj": "m.gguf"}} + got = server.with_current_reader(cfg, new) + self.assertEqual(got["vision"]["exe"], str(new / "strata-vision.exe")) + self.assertEqual(got["vision"]["mmproj"], "m.gguf") + + def test_a_missing_reader_is_still_replaced(self): + with tempfile.TemporaryDirectory() as tmp: + new = self._package(tmp, "0.1.54-linux-x86_64-cuda-cuda13") + gone = Path(tmp) / "components" / "engine" / "0.1.52-linux-x86_64-cuda-cuda13" / "strata-vision.exe" + cfg = {"vision": {"exe": str(gone)}} + got = server.with_current_reader(cfg, new) + self.assertEqual(got["vision"]["exe"], str(new / "strata-vision.exe")) + + def test_a_reader_outside_the_engine_packages_is_left_alone(self): + with tempfile.TemporaryDirectory() as tmp: + new = self._package(tmp, "0.1.54-linux-x86_64-cuda-cuda13") + own = Path(tmp) / "my-encoders" / "strata-vision-q8" + own.parent.mkdir(parents=True) + own.write_bytes(b"mine") + cfg = {"vision": {"exe": str(own)}} + self.assertIs(server.with_current_reader(cfg, new), cfg) + + def test_the_current_package_is_left_alone(self): + with tempfile.TemporaryDirectory() as tmp: + new = self._package(tmp, "0.1.54-linux-x86_64-cuda-cuda13") + cfg = {"vision": {"exe": str(new / "strata-vision.exe")}} + self.assertIs(server.with_current_reader(cfg, new), cfg) + + if __name__ == "__main__": unittest.main()