feat(standalone): 新增单机模式,KILOSTAR_MODE=standalone 时去掉 Ray 依赖

通过 StandaloneProxy 适配层让 .remote() 调用在单机模式下透明降级为
asyncio 协程调用,7 个 Actor 和 workflow task 均可在纯 asyncio 环境运行,
启动快、资源占用低。分布式模式行为完全不变。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 15:52:41 +00:00
parent 76a67e8237
commit 457d12834f
14 changed files with 390 additions and 108 deletions
+68 -44
View File
@@ -11,9 +11,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import time
import ray
from functools import lru_cache
from typing import Any, Dict
_STANDALONE = os.environ.get("KILOSTAR_MODE", "distributed") == "standalone"
if not _STANDALONE:
import ray
class ActorList:
@@ -37,62 +43,80 @@ class ActorList:
raise AttributeError(f"ActorList对象没有属性 '{key}'")
@lru_cache(maxsize=128)
def _get_cached_actor_handle(actor_name: str):
"""缓存接口"""
return ray.get_actor(actor_name, namespace="kilostar")
# ─── Standalone Registry ───
_standalone_registry: Dict[str, Any] = {}
def clear_actor_cache():
"""清理接口"""
_get_cached_actor_handle.cache_clear()
def register_standalone(name: str, instance: Any) -> None:
"""注册一个单机模式下的 Actor 单例(已包装为 StandaloneProxy)。"""
from kilostar.utils.standalone_proxy import StandaloneProxy
_standalone_registry[name] = StandaloneProxy(instance)
def wait_for_actor(
actor_name: str, *, timeout: float = 10.0, interval: float = 0.5
):
"""阻塞等待某个 actor 就绪,返回其句柄。
# ─── Distributed Mode Helpers ───
用于"启动期 / ray task 入口刚拉起"这类场景——被依赖的 actor 可能还没注册。
在 ``timeout`` 内按 ``interval`` 轮询 ``ray.get_actor``;拿到就立即返回,
超时则抛带清晰上下文的 ``TimeoutError``(而不是裸 ``ValueError``)。
Args:
actor_name: actor 注册名
timeout: 最长等待秒数;``<=0`` 表示只试一次(等价于直接取句柄)
interval: 轮询间隔秒数
if not _STANDALONE:
Raises:
TimeoutError: 超时仍未就绪。原始异常通过 ``raise ... from`` 链保留。
"""
deadline = time.monotonic() + max(timeout, 0.0)
last_err: Exception | None = None
while True:
try:
return _get_cached_actor_handle(actor_name)
except Exception as e: # ray.get_actor 失败一般是 ValueError
last_err = e
# 失败不能让 lru_cache 留下脏数据(异常本身不会被缓存,
# 但若底层换实现,这里清一次更稳妥)
if time.monotonic() >= deadline:
raise TimeoutError(
f"等待 actor {actor_name!r} 就绪超时({timeout}s):{last_err}"
) from last_err
time.sleep(interval)
@lru_cache(maxsize=128)
def _get_cached_actor_handle(actor_name: str):
"""缓存接口"""
return ray.get_actor(actor_name, namespace="kilostar")
def clear_actor_cache():
"""清理接口"""
_get_cached_actor_handle.cache_clear()
def wait_for_actor(
actor_name: str, *, timeout: float = 10.0, interval: float = 0.5
):
"""阻塞等待某个 actor 就绪,返回其句柄。"""
deadline = time.monotonic() + max(timeout, 0.0)
last_err: Exception | None = None
while True:
try:
return _get_cached_actor_handle(actor_name)
except Exception as e:
last_err = e
if time.monotonic() >= deadline:
raise TimeoutError(
f"等待 actor {actor_name!r} 就绪超时({timeout}s):{last_err}"
) from last_err
time.sleep(interval)
else:
def _get_cached_actor_handle(actor_name: str):
raise RuntimeError("单机模式下不应调用 _get_cached_actor_handle")
def clear_actor_cache():
pass
def wait_for_actor(actor_name: str, **kwargs):
raise RuntimeError("单机模式下不应调用 wait_for_actor")
# ─── 统一入口 ───
def ray_actor_hook(*actor_names: str, timeout: float = 0.0, interval: float = 0.5):
"""按名字批量取出 Ray Actor 句柄,组装成一个 ``ActorList`` 返回。
"""按名字批量取出 Actor 句柄,组装成一个 ActorList 返回。
例:``actors = ray_actor_hook("postgres_database", "global_state_machine")``
随后即可用 ``actors.postgres_database`` 拿到对应句柄。
Args:
timeout: ``>0`` 时对每个 actor 走 ``wait_for_actor`` 等待就绪(启动期用);
缺省 ``0`` 保持原"快速失败"语义——actor 不在立即抛异常。
interval: 等待轮询间隔,仅在 ``timeout>0`` 时生效。
单机模式从 _standalone_registry 取,分布式模式走 ray.get_actor。
"""
actor_list = ActorList()
if _STANDALONE:
for name in actor_names:
if name not in _standalone_registry:
raise ValueError(
f"Standalone registry: actor {name!r} not registered"
)
setattr(actor_list, name, _standalone_registry[name])
return actor_list
for actor_name in actor_names:
if timeout > 0:
handle = wait_for_actor(
+86
View File
@@ -0,0 +1,86 @@
"""KiloStar 单机模式适配层:用 asyncio 协程模拟 Ray Actor 接口。
单机模式下,所有 Actor 退化为普通 Python 异步单例,通过 StandaloneProxy
包装后暴露与 Ray Actor Handle 相同的 `.method.remote(args)` 调用接口,
使上层代码在两种模式间无感切换。
"""
from __future__ import annotations
import asyncio
import os
from typing import Any
_STANDALONE = os.environ.get("KILOSTAR_MODE", "distributed") == "standalone"
class _MethodProxy:
"""包装单个方法,使 .remote(*args, **kwargs) 返回一个可 await 的 Task。"""
__slots__ = ("_method",)
def __init__(self, method: Any):
self._method = method
def remote(self, *args: Any, **kwargs: Any) -> asyncio.Task:
async def _invoke():
result = self._method(*args, **kwargs)
if asyncio.iscoroutine(result):
return await result
return result
return asyncio.ensure_future(_invoke())
class StandaloneProxy:
"""包装一个普通 Python 实例,模拟 Ray Actor Handle 的属性访问接口。
用法:proxy.some_method.remote(x, y) → 等效于 await instance.some_method(x, y)
"""
__slots__ = ("_instance",)
def __init__(self, instance: Any):
object.__setattr__(self, "_instance", instance)
def __getattr__(self, name: str) -> _MethodProxy:
attr = getattr(object.__getattribute__(self, "_instance"), name)
if callable(attr):
return _MethodProxy(attr)
return attr
# ─── 条件装饰器 ───
def actor_class(cls):
"""条件装饰器:分布式模式 → @ray.remote,单机模式 → 原样返回类。"""
if _STANDALONE:
return cls
import ray
return ray.remote(cls)
def remote_task(func):
"""条件装饰器:分布式 → @ray.remote(func),单机 → .remote() 转为 asyncio task。
单机模式下返回一个 stub 对象,其 .remote() 方法把函数以协程方式调度到
当前事件循环(workflow task 需要用 await 版本的 _entry,由调用方处理)。
"""
if _STANDALONE:
class _TaskProxy:
@staticmethod
def remote(*args, **kwargs):
async def _run():
result = func(*args, **kwargs)
if asyncio.iscoroutine(result):
return await result
return result
return asyncio.ensure_future(_run())
return _TaskProxy()
import ray
return ray.remote(func)