已合并
【msserviceprofiler】【需求】【Tracing 2/3】接入vLLM Hook Tracing埋点并支持Jaeger链路展示 #447
【msserviceprofiler】【需求】【Tracing 2/3】接入vLLM Hook Tracing埋点并支持Jaeger链路展示 #447
已合并
ChaseChe77创建于 7 天前
13 个文件变更+837-52
@@ -120,6 +120,11 @@ def _metrics_noop_handler(original_func, *args, **kwargs):
120 return original_func(*args, **kwargs)120 return original_func(*args, **kwargs)
121 121 
122 122 
123+def _profiling_noop_handler(original_func, *args, **kwargs):
124+ """Trace-only passthrough; it keeps profiling handlers out of the process."""
125+ return original_func(*args, **kwargs)
126+ 
127+ 
123def _resolve_metrics_handler_func(symbol_info: dict, method_name: str) -> Callable:128def _resolve_metrics_handler_func(symbol_info: dict, method_name: str) -> Callable:
124 """解析 metrics 配置的 handler:无 handler_path 时用 wrap_handler_with_metrics 封装透传函数;129 """解析 metrics 配置的 handler:无 handler_path 时用 wrap_handler_with_metrics 封装透传函数;
125 有 handler_path 且为 "module:func" 时导入并直接返回该函数(不再包装)。130 有 handler_path 且为 "module:func" 时导入并直接返回该函数(不再包装)。
@@ -158,6 +163,30 @@ class PatternEntry:
158 caller_filter: Optional[str] = None163 caller_filter: Optional[str] = None
159 need_locals: bool = False164 need_locals: bool = False
160 pattern_id: str = ""165 pattern_id: str = ""
166+ around_hook_factory: Optional[Callable] = None
167+ 
168+ 
169+def _make_trace_factory(item, method_name, symbol_path, traced_symbols, enable_tracing):
170+ """Build one tracing wrapper while keeping profiling parsing shallow."""
171+ if not enable_tracing:
172+ return None
173+ 
174+ from .trace_hook import make_trace_around_factory, parse_hook_trace_spec
175+ 
176+ try:
177+ trace_spec = parse_hook_trace_spec(item, method_name)
178+ except ValueError as exc:
179+ logger.warning("Skip invalid tracing metadata for %s: %s", symbol_path, exc)
180+ return None
181+ 
182+ if trace_spec is None:
183+ return None
184+ if symbol_path in traced_symbols:
185+ logger.warning("Ignore duplicate tracing definition for %s", symbol_path)
186+ return None
187+ 
188+ traced_symbols.add(symbol_path)
189+ return make_trace_around_factory(trace_spec)
161 190 
162 191 
163def _merge_config_impl(192def _merge_config_impl(
@@ -228,7 +257,7 @@ class ConfigLoader:
228 self._config_path = config_path257 self._config_path = config_path
229 self._framework_version = framework_version258 self._framework_version = framework_version
230 259 
231- def load_profiling(self) -> ProfilingConfig:260+ def load_profiling(self, enable_profiling: bool = True, enable_tracing: bool = False) -> ProfilingConfig:
232 """加载 profiling yml 配置并解析为 Handler 列表与模式列表。261 """加载 profiling yml 配置并解析为 Handler 列表与模式列表。
233 262 
234 Returns:263 Returns:
@@ -244,21 +273,28 @@ class ConfigLoader:
244 273 
245 result: Dict[str, List[ConfigHooker]] = {}274 result: Dict[str, List[ConfigHooker]] = {}
246 pattern_entries: List[PatternEntry] = []275 pattern_entries: List[PatternEntry] = []
247- 276+ traced_symbols = set()
248 for item in raw_config:277 for item in raw_config:
249 if not isinstance(item, dict) or 'symbol' not in item:278 if not isinstance(item, dict) or 'symbol' not in item:
250 logger.warning("Skip invalid config item: missing 'symbol'")279 logger.warning("Skip invalid config item: missing 'symbol'")
251 continue280 continue
252 281 
253 symbol_path = item['symbol']282 symbol_path = item['symbol']
254- need_locals = "expr" in json.dumps(item) or "handler" in json.dumps(item)283+ # Trace adapters use their independent around-call wrapper. Keep the
284+ # bytecode/local-variable path exclusive to existing profiling handlers.
285+ need_locals = enable_profiling and ("expr" in json.dumps(item) or "handler" in json.dumps(item))
255 286 
256 if _is_pattern_symbol(symbol_path):287 if _is_pattern_symbol(symbol_path):
257 parsed = _parse_symbol_pattern(symbol_path)288 parsed = _parse_symbol_pattern(symbol_path)
258 if not parsed:289 if not parsed:
259 continue290 continue
260 module_pattern, class_pattern, method_name = parsed291 module_pattern, class_pattern, method_name = parsed
261- handler_func = _resolve_handler_func(item, method_name)292+ handler_func = _resolve_handler_func(item, method_name) if enable_profiling else _profiling_noop_handler
293+ around_hook_factory = _make_trace_factory(
294+ item, method_name, symbol_path, traced_symbols, enable_tracing
295+ )
296+ if not enable_profiling and around_hook_factory is None:
297+ continue
262 name = item.get('name', method_name)298 name = item.get('name', method_name)
263 domain = item.get('domain', 'Default')299 domain = item.get('domain', 'Default')
264 pattern_entries.append(300 pattern_entries.append(
@@ -274,6 +310,7 @@ class ConfigLoader:
274 caller_filter=item.get('caller_filter'),310 caller_filter=item.get('caller_filter'),
275 need_locals=need_locals,311 need_locals=need_locals,
276 pattern_id=symbol_path,312 pattern_id=symbol_path,
313+ around_hook_factory=around_hook_factory,
277 )314 )
278 )315 )
279 continue316 continue
@@ -283,7 +320,10 @@ class ConfigLoader:
283 continue320 continue
284 321 
285 hook_points = _build_hook_points(module_path, method_name, class_name)322 hook_points = _build_hook_points(module_path, method_name, class_name)
286- handler_func = _resolve_handler_func(item, method_name)323+ handler_func = _resolve_handler_func(item, method_name) if enable_profiling else _profiling_noop_handler
324+ around_hook_factory = _make_trace_factory(item, method_name, symbol_path, traced_symbols, enable_tracing)
325+ if not enable_profiling and around_hook_factory is None:
326+ continue
287 327 
288 handler_instance = ConfigHooker(328 handler_instance = ConfigHooker(
289 hook_list=hook_points,329 hook_list=hook_points,
@@ -294,6 +334,7 @@ class ConfigLoader:
294 caller_filter=item.get('caller_filter'),334 caller_filter=item.get('caller_filter'),
295 need_locals=need_locals,335 need_locals=need_locals,
296 framework_version=self._framework_version,336 framework_version=self._framework_version,
337+ around_hook_factory=around_hook_factory,
297 )338 )
298 339 
299 if symbol_path not in result:340 if symbol_path not in result:
@@ -88,6 +88,7 @@ class ConfigHooker:
88 caller_filter: Optional[str],88 caller_filter: Optional[str],
89 need_locals: bool = False,89 need_locals: bool = False,
90 framework_version: Optional[str] = None,90 framework_version: Optional[str] = None,
91+ around_hook_factory: Optional[Callable] = None,
91 ):92 ):
92 """初始化 ConfigHooker。93 """初始化 ConfigHooker。
93 94 
@@ -108,6 +109,7 @@ class ConfigHooker:
108 self.caller_filter = caller_filter109 self.caller_filter = caller_filter
109 self.need_locals = need_locals110 self.need_locals = need_locals
110 self.framework_version = framework_version111 self.framework_version = framework_version
112+ self.around_hook_factory = around_hook_factory
111 113 
112 # hook_func 改为支持多个,一个hook点位支持多个hook函数114 # hook_func 改为支持多个,一个hook点位支持多个hook函数
113 hook_funcs = hook_func if isinstance(hook_func, list) else [hook_func]115 hook_funcs = hook_func if isinstance(hook_func, list) else [hook_func]
@@ -295,6 +297,18 @@ class MultiHandlerDynamicHooker(DynamicHooker):
295 super().__init__(hook_list, hook_func, min_version, max_version, caller_filter, need_locals)297 super().__init__(hook_list, hook_func, min_version, max_version, caller_filter, need_locals)
296 self.handlers = set()298 self.handlers = set()
297 299 
300+ def select_around_hook_factory(self, handlers):
301+ factories = [
302+ factory
303+ for factory in (getattr(handler, "around_hook_factory", None) for handler in handlers)
304+ if factory is not None
305+ ]
306+ if len(factories) > 1:
307+ logger.warning("Multiple tracing definitions resolved for one symbol; only one will be applied")
308+ if self.around_hook_factory in factories:
309+ return self.around_hook_factory
310+ return factories[0] if factories else None
311+ 
298 def build_wrap_hook_func(self, handler_wrap_hook_funcs):312 def build_wrap_hook_func(self, handler_wrap_hook_funcs):
299 handler_wrap_hook_funcs = [313 handler_wrap_hook_funcs = [
300 x for x in handler_wrap_hook_funcs if x is not None and x != VLLMHookerBase.default_hook_func314 x for x in handler_wrap_hook_funcs if x is not None and x != VLLMHookerBase.default_hook_func
@@ -338,6 +352,7 @@ class MultiHandlerDynamicHooker(DynamicHooker):
338 self.handlers.add(handler)352 self.handlers.add(handler)
339 self.wrap_hook_func = self.build_wrap_hook_func(x.wrap_hook_func for x in self.handlers)353 self.wrap_hook_func = self.build_wrap_hook_func(x.wrap_hook_func for x in self.handlers)
340 self.context_hook_funcs = sum((x.context_hook_funcs for x in self.handlers), [])354 self.context_hook_funcs = sum((x.context_hook_funcs for x in self.handlers), [])
355+ self.around_hook_factory = self.select_around_hook_factory(self.handlers)
341 self.need_locals = any((x.need_locals for x in self.handlers))356 self.need_locals = any((x.need_locals for x in self.handlers))
342 self.init()357 self.init()
343 358 
@@ -352,6 +367,7 @@ class MultiHandlerDynamicHooker(DynamicHooker):
352 367 
353 self.wrap_hook_func = self.build_wrap_hook_func(x.wrap_hook_func for x in self.handlers)368 self.wrap_hook_func = self.build_wrap_hook_func(x.wrap_hook_func for x in self.handlers)
354 self.context_hook_funcs = sum((x.context_hook_funcs for x in self.handlers), [])369 self.context_hook_funcs = sum((x.context_hook_funcs for x in self.handlers), [])
370+ self.around_hook_factory = self.select_around_hook_factory(self.handlers)
355 self.need_locals = any((x.need_locals for x in self.handlers))371 self.need_locals = any((x.need_locals for x in self.handlers))
356 self.init()372 self.init()
357 373 
@@ -41,9 +41,8 @@ def _get_metric_hook_chain_getter():
41 return _GET_CHAIN_FUNC41 return _GET_CHAIN_FUNC
42 42 
43 try:43 try:
44- from ms_service_metric.core.hook.hook_chain import get_chain as imported_get_chain44+ hook_chain_module = importlib.import_module("ms_service_metric.core.hook.hook_chain")
45- 45+ _GET_CHAIN_FUNC = hook_chain_module.get_chain
46- _GET_CHAIN_FUNC = imported_get_chain
47 return _GET_CHAIN_FUNC46 return _GET_CHAIN_FUNC
48 except Exception:47 except Exception:
49 return None48 return None
@@ -461,6 +460,10 @@ class VLLMHookerBase(ABC):
461 # 对应的 hook 处理函数,用于配置化时复用460 # 对应的 hook 处理函数,用于配置化时复用
462 self.wrap_hook_func: Optional[Callable] = None461 self.wrap_hook_func: Optional[Callable] = None
463 self.context_hook_funcs: List[Callable] = []462 self.context_hook_funcs: List[Callable] = []
463+ # Optional outer wrapper used by independent instrumentation such as
464+ # Hook tracing. It is applied after the existing profiling callable
465+ # is built, so the profiling/context-hook execution model is unchanged.
466+ self.around_hook_factory: Optional[Callable] = None
464 self.need_locals = False467 self.need_locals = False
465 468 
466 @abstractmethod469 @abstractmethod
@@ -600,11 +603,16 @@ class VLLMHookerBase(ABC):
600 603 
601 if self.wrap_hook_func == VLLMHookerBase.default_hook_func:604 if self.wrap_hook_func == VLLMHookerBase.default_hook_func:
602 # 如果都没有原始的 wrap_hook_func, 就直接使用原函数,拜托一层一层的封装605 # 如果都没有原始的 wrap_hook_func, 就直接使用原函数,拜托一层一层的封装
603- profiler_func = ori_func606+ # An around hook must call the trackable wrapper so a business
607+ # exception is never retried as an instrumentation failure.
608+ profiler_func = trackable_ori_func if self.around_hook_factory else ori_func
604 else:609 else:
605 # 如果有原始的 wrap_hook_func, 就使用修改前的方式610 # 如果有原始的 wrap_hook_func, 就使用修改前的方式
606 profiler_func = profiler_func_maker(trackable_ori_func)611 profiler_func = profiler_func_maker(trackable_ori_func)
607 612 
613+ if self.around_hook_factory is not None:
614+ profiler_func = self.around_hook_factory(profiler_func, ori_func)
615+ 
608 if hook_node is not None:616 if hook_node is not None:
609 617 
610 def _recover_current(cur_hook_ref=hook_node):618 def _recover_current(cur_hook_ref=hook_node):
@@ -772,6 +780,8 @@ class VLLMHookerBase(ABC):
772 logger.debug(f"calling profiler_func={self.applied_hook_func_name} for {ori_func}")780 logger.debug(f"calling profiler_func={self.applied_hook_func_name} for {ori_func}")
773 return await profiler_func(*args, **kwargs)781 return await profiler_func(*args, **kwargs)
774 except Exception as e:782 except Exception as e:
783+ if trackable_ori_func.executed and trackable_ori_func.cached_exception is e:
784+ raise
775 failures += 1785 failures += 1
776 self._log_hook_exception(trackable_ori_func, e, failures)786 self._log_hook_exception(trackable_ori_func, e, failures)
777 787 
@@ -835,6 +845,8 @@ class VLLMHookerBase(ABC):
835 logger.debug(f"calling profiler_func={self.applied_hook_func_name} for {ori_func}")845 logger.debug(f"calling profiler_func={self.applied_hook_func_name} for {ori_func}")
836 return profiler_func(*args, **kwargs)846 return profiler_func(*args, **kwargs)
837 except Exception as e:847 except Exception as e:
848+ if trackable_ori_func.executed and trackable_ori_func.cached_exception is e:
849+ raise
838 failures += 1850 failures += 1
839 self._log_hook_exception(trackable_ori_func, e, failures)851 self._log_hook_exception(trackable_ori_func, e, failures)
840 852 
@@ -51,10 +51,7 @@ def discover_classes_with_method(module: Any, method_name: str, module_fullname:
51 meth = getattr(cls, method_name, None)51 meth = getattr(cls, method_name, None)
52 if not callable(meth):52 if not callable(meth):
53 continue53 continue
54- try:54+ if getattr(meth, "__module__", None) != module_fullname:
55- if getattr(meth, "__module__", None) != module_fullname:
56- continue
57- except Exception:
58 continue55 continue
59 out.append((cls.__name__, method_name))56 out.append((cls.__name__, method_name))
60 return out57 return out
@@ -164,6 +161,7 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
164 max_version=entry.max_version,161 max_version=entry.max_version,
165 caller_filter=entry.caller_filter,162 caller_filter=entry.caller_filter,
166 need_locals=entry.need_locals,163 need_locals=entry.need_locals,
164+ around_hook_factory=entry.around_hook_factory,
167 )165 )
168 handler.register()166 handler.register()
169 self._prepared_hookers.add(handler)167 self._prepared_hookers.add(handler)
@@ -305,6 +303,9 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
305 class LoaderWrapper(importlib.abc.Loader):303 class LoaderWrapper(importlib.abc.Loader):
306 _vllm_profiler_wrapped = True304 _vllm_profiler_wrapped = True
307 305 
306+ def __init__(self, finder):
307+ self._finder = finder
308+ 
308 def create_module(self, spec):309 def create_module(self, spec):
309 if hasattr(orig_loader, "create_module"):310 if hasattr(orig_loader, "create_module"):
310 return orig_loader.create_module(spec)311 return orig_loader.create_module(spec)
@@ -314,8 +315,7 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
314 orig_loader.exec_module(module)315 orig_loader.exec_module(module)
315 self._finder.on_symbol_module_loaded(fullname)316 self._finder.on_symbol_module_loaded(fullname)
316 317 
317- wrapper = LoaderWrapper()318+ wrapper = LoaderWrapper(self)
318- wrapper._finder = self
319 spec.loader = wrapper319 spec.loader = wrapper
320 return spec320 return spec
321 321 
@@ -335,7 +335,12 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
335 matching = [e for e in all_pattern_entries if _pattern_matches_module(e.module_pattern, fullname)]335 matching = [e for e in all_pattern_entries if _pattern_matches_module(e.module_pattern, fullname)]
336 if concrete_match or matching:336 if concrete_match or matching:
337 self._module_matching_pattern_cache[fullname] = [337 self._module_matching_pattern_cache[fullname] = [
338- (e, self._pattern_to_concrete_profiling if e in self._pattern_handlers_profiling else self._pattern_to_concrete_metrics)338+ (
339+ e,
340+ self._pattern_to_concrete_profiling
341+ if e in self._pattern_handlers_profiling
342+ else self._pattern_to_concrete_metrics,
343+ )
339 for e in matching344 for e in matching
340 ]345 ]
341 return True346 return True
@@ -343,7 +348,7 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
343 348 
344 def on_symbol_module_loaded(self, fullname: str):349 def on_symbol_module_loaded(self, fullname: str):
345 """当 symbol 模块加载完成时的回调。"""350 """当 symbol 模块加载完成时的回调。"""
346- logger.debug(f"SymbolWatchFinder: Module loaded callback for {fullname}")351+ logger.debug("SymbolWatchFinder: Module loaded callback for %s", fullname)
347 self._prepare_hooks_for_module(fullname)352 self._prepare_hooks_for_module(fullname)
348 353 
349 def _prepare_hooks_for_module(self, fullname: str):354 def _prepare_hooks_for_module(self, fullname: str):
@@ -363,12 +368,12 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
363 continue368 continue
364 discovered = discover_classes_with_method(module_obj, entry.method_name, fullname)369 discovered = discover_classes_with_method(module_obj, entry.method_name, fullname)
365 for class_name, method_name in discovered:370 for class_name, method_name in discovered:
366- self._apply_one_pattern_hook(371+ self._apply_one_pattern_hook(fullname, class_name, method_name, entry, pattern_to_concrete)
367- fullname, class_name, method_name, entry, pattern_to_concrete
368- )
369 372 
370 if module_handlers:373 if module_handlers:
371- logger.debug(f"Detected symbol module loaded: {fullname}, preparing {len(module_handlers)} handler groups")374+ logger.debug(
375+ "Detected symbol module loaded: %s, preparing %d handler groups", fullname, len(module_handlers)
376+ )
372 self._prepare_handlers_for_module(fullname, module_handlers)377 self._prepare_handlers_for_module(fullname, module_handlers)
373 378 
374 def _prepare_handlers_for_module(self, module_name: str, module_handlers: List[Tuple[str, List]]):379 def _prepare_handlers_for_module(self, module_name: str, module_handlers: List[Tuple[str, List]]):
@@ -386,19 +391,19 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
386 with self._lock:391 with self._lock:
387 if handler not in self._applied_hookers:392 if handler not in self._applied_hookers:
388 self._applied_hookers.append(handler)393 self._applied_hookers.append(handler)
389- logger.debug(f"Auto-applied handler for symbol {symbol_path}")394+ logger.debug("Auto-applied handler for symbol %s", symbol_path)
390 except Exception as e:395 except Exception as e:
391- logger.error(f"Failed to auto-apply handler for {symbol_path}: {e}")396+ logger.error("Failed to auto-apply handler for %s: %s", symbol_path, e)
392 self._symbol_to_hooker[symbol_path] = hookers_for_symbol397 self._symbol_to_hooker[symbol_path] = hookers_for_symbol
393 self._applied_hooks.add(symbol_path)398 self._applied_hooks.add(symbol_path)
394- logger.debug(f"Prepared {len(handler_list)} handler(s) for symbol {symbol_path}")399+ logger.debug("Prepared %d handler(s) for symbol %s", len(handler_list), symbol_path)
395 except Exception as e:400 except Exception as e:
396- logger.error(f"Failed to prepare handlers for module {module_name}: {e}")401+ logger.error("Failed to prepare handlers for module %s: %s", module_name, e)
397 402 
398 def apply_all_hooks(self):403 def apply_all_hooks(self):
399 """应用所有准备好的 hooks。"""404 """应用所有准备好的 hooks。"""
400 self.set_auto_apply(True)405 self.set_auto_apply(True)
401- logger.info(f"Applying {len(self._prepared_hookers)} prepared hooks...")406+ logger.info("Applying %d prepared hooks...", len(self._prepared_hookers))
402 applied_now = 0407 applied_now = 0
403 for hooker in list(self._prepared_hookers):408 for hooker in list(self._prepared_hookers):
404 try:409 try:
@@ -407,10 +412,10 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
407 if hooker not in self._applied_hookers:412 if hooker not in self._applied_hookers:
408 self._applied_hookers.append(hooker)413 self._applied_hookers.append(hooker)
409 applied_now += 1414 applied_now += 1
410- logger.debug(f"Applied hooker: {hooker.applied_hook_func_name}")415+ logger.debug("Applied hooker: %s", hooker.applied_hook_func_name)
411 except Exception as e:416 except Exception as e:
412- logger.error(f"Failed to apply hooker {hooker.applied_hook_func_name}: {e}")417+ logger.error("Failed to apply hooker %s: %s", hooker.applied_hook_func_name, e)
413- logger.info(f"Successfully applied {applied_now} hooks")418+ logger.info("Successfully applied %d hooks", applied_now)
414 return self.get_applied_hookers()419 return self.get_applied_hookers()
415 420 
416 def check_and_apply_existing_modules(self) -> bool:421 def check_and_apply_existing_modules(self) -> bool:
@@ -422,7 +427,7 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
422 module_path = symbol_path.split(":")[0]427 module_path = symbol_path.split(":")[0]
423 if module_path in sys.modules and module_path not in seen:428 if module_path in sys.modules and module_path not in seen:
424 seen.add(module_path)429 seen.add(module_path)
425- logger.debug(f"Module {module_path} already loaded, preparing handlers")430+ logger.debug("Module %s already loaded, preparing handlers", module_path)
426 self.on_symbol_module_loaded(module_path)431 self.on_symbol_module_loaded(module_path)
427 # pattern symbol处理逻辑432 # pattern symbol处理逻辑
428 for fullname in list(sys.modules.keys()):433 for fullname in list(sys.modules.keys()):
@@ -430,6 +435,6 @@ class SymbolWatchFinder(importlib.abc.MetaPathFinder):
430 continue435 continue
431 if self._is_target_symbol(fullname):436 if self._is_target_symbol(fullname):
432 seen.add(fullname)437 seen.add(fullname)
433- logger.debug(f"Module {fullname} matches pattern, preparing handlers")438+ logger.debug("Module %s matches pattern, preparing handlers", fullname)
434 self.on_symbol_module_loaded(fullname)439 self.on_symbol_module_loaded(fullname)
435 return True440 return True
@@ -0,0 +1,209 @@
1+# -------------------------------------------------------------------------
2+# This file is part of the MindStudio project.
3+# Copyright (c) 2026 Huawei Technologies Co.,Ltd.
4+#
5+# MindStudio is licensed under Mulan PSL v2.
6+# -------------------------------------------------------------------------
7+ 
8+"""Thin around-call adapters that add business semantics to vLLM OTel spans."""
9+ 
10+import functools
11+import inspect
12+import time
13+from contextlib import contextmanager
14+from dataclasses import dataclass
15+from typing import Any, Dict, Iterable
16+ 
17+from ms_service_profiler.tracer.hook_runtime import get_hook_trace_runtime
18+ 
19+ 
20+SUPPORTED_KINDS = {"INTERNAL", "SERVER", "CLIENT", "PRODUCER", "CONSUMER"}
21+SUPPORTED_ADAPTERS = {"call", "request", "request_context", "schedule", "model", "output"}
22+ 
23+ 
24+@dataclass(frozen=True)
25+class HookTraceSpec:
26+ name: str
27+ domain: str
28+ kind: str = "INTERNAL"
29+ adapter: str = "call"
30+ 
31+ 
32+@dataclass
33+class _TraceInvocation:
34+ args: tuple
35+ kwargs: dict
36+ return_value: Any = None
37+ 
38+ 
39+def parse_hook_trace_spec(item: Dict[str, Any], method_name: str):
40+ raw = item.get("trace")
41+ if raw is None or raw is False:
42+ return None
43+ if raw is True:
44+ raw = {}
45+ if not isinstance(raw, dict):
46+ raise ValueError("trace must be a mapping, true, false, or omitted")
47+ name = raw.get("name", item.get("name", method_name))
48+ domain = raw.get("domain", item.get("domain", "Tracing"))
49+ kind = str(raw.get("kind", "INTERNAL")).upper()
50+ adapter = raw.get("adapter", "call")
51+ if not isinstance(name, str) or not name:
52+ raise ValueError("trace.name must be a non-empty string")
53+ if not isinstance(domain, str) or not domain:
54+ raise ValueError("trace.domain must be a non-empty string")
55+ if kind not in SUPPORTED_KINDS:
56+ raise ValueError("trace.kind is unsupported")
57+ if adapter not in SUPPORTED_ADAPTERS:
58+ raise ValueError("trace.adapter is unsupported")
59+ return HookTraceSpec(name, domain, kind, adapter)
60+ 
61+ 
62+def _request_id_from_call(invocation: _TraceInvocation) -> str:
63+ if "request_id" in invocation.kwargs:
64+ return str(invocation.kwargs["request_id"])
65+ if len(invocation.args) > 1:
66+ candidate = invocation.args[1]
67+ request_id = getattr(candidate, "request_id", candidate)
68+ if isinstance(request_id, (str, int)):
69+ return str(request_id)
70+ return ""
71+ 
72+ 
73+def _trace_headers_from_call(invocation: _TraceInvocation):
74+ headers = invocation.kwargs.get("trace_headers")
75+ if headers:
76+ return headers
77+ for candidate in invocation.args[1:]:
78+ headers = getattr(candidate, "trace_headers", None)
79+ if headers:
80+ return headers
81+ return None
82+ 
83+ 
84+def _request_ids_from_scheduler_output(output) -> Iterable[str]:
85+ return list((getattr(output, "num_scheduled_tokens", None) or {}).keys())
86+ 
87+ 
88+def _request_ids_from_engine_outputs(outputs) -> Iterable[str]:
89+ result = []
90+ for output in outputs or []:
91+ request_id = getattr(output, "request_id", None)
92+ if request_id is not None:
93+ result.append(str(request_id))
94+ return result
95+ 
96+ 
97+def _request_ids_on_enter(spec: HookTraceSpec, invocation: _TraceInvocation) -> Iterable[str]:
98+ if spec.adapter == "model" and len(invocation.args) > 1:
99+ return _request_ids_from_scheduler_output(invocation.args[1])
100+ if spec.adapter == "output" and len(invocation.args) > 1:
101+ return _request_ids_from_engine_outputs(invocation.args[1])
102+ return []
103+ 
104+ 
105+def _add_exit_semantics(runtime, span, spec: HookTraceSpec, invocation: _TraceInvocation) -> None:
106+ if spec.adapter == "schedule":
107+ request_ids = list(_request_ids_from_scheduler_output(invocation.return_value))
108+ span.set_attribute("batch.request_count", len(request_ids))
109+ total_tokens = getattr(invocation.return_value, "total_num_scheduled_tokens", None)
110+ if total_tokens is not None:
111+ span.set_attribute("batch.scheduled_tokens", total_tokens)
112+ 
113+ if spec.adapter == "output" and len(invocation.args) > 1:
114+ for output in invocation.args[1] or []:
115+ if getattr(output, "finish_reason", None) is not None:
116+ runtime.finish_request(getattr(output, "request_id", ""), True)
117+ 
118+ 
119+@contextmanager
120+def _trace_scope(spec: HookTraceSpec, invocation: _TraceInvocation):
121+ """Surround one call without using the profiling context-hook engine."""
122+ runtime = get_hook_trace_runtime()
123+ if not runtime.enabled:
124+ yield
125+ return
126+ 
127+ if spec.adapter == "request":
128+ request_id = _request_id_from_call(invocation)
129+ if request_id:
130+ runtime.start_request(request_id, _trace_headers_from_call(invocation))
131+ try:
132+ yield
133+ except BaseException:
134+ if request_id:
135+ runtime.finish_request(request_id)
136+ raise
137+ return
138+ 
139+ if spec.adapter == "request_context":
140+ request_id = _request_id_from_call(invocation)
141+ if request_id:
142+ runtime.register_request_context(request_id, _trace_headers_from_call(invocation))
143+ yield
144+ return
145+ 
146+ # OTel links are immutable. Scheduler request IDs are available only in
147+ # its return value, so create the completed span after the call.
148+ if spec.adapter == "schedule":
149+ start_time_ns = time.time_ns()
150+ try:
151+ yield
152+ except BaseException as exception:
153+ span = runtime.start_span(spec.name, spec.domain, spec.kind, start_time_ns=start_time_ns)
154+ span.end(False, str(exception), time.time_ns())
155+ raise
156+ else:
157+ request_ids = list(_request_ids_from_scheduler_output(invocation.return_value))
158+ span = runtime.start_span(
159+ spec.name,
160+ spec.domain,
161+ spec.kind,
162+ request_ids=request_ids,
163+ start_time_ns=start_time_ns,
164+ )
165+ _add_exit_semantics(runtime, span, spec, invocation)
166+ span.end(True, end_time_ns=time.time_ns())
167+ return
168+ 
169+ request_ids = list(_request_ids_on_enter(spec, invocation))
170+ span = runtime.start_span(spec.name, spec.domain, spec.kind, request_ids=request_ids)
171+ token = runtime.activate(span)
172+ try:
173+ yield
174+ except BaseException as exception:
175+ span.end(False, str(exception))
176+ raise
177+ else:
178+ _add_exit_semantics(runtime, span, spec, invocation)
179+ span.end(True)
180+ finally:
181+ runtime.deactivate(token)
182+ 
183+ 
184+def make_trace_around_factory(spec: HookTraceSpec):
185+ """Create one outer wrapper factory; it never changes profiling handlers."""
186+ 
187+ def factory(next_func, original_func):
188+ if inspect.iscoroutinefunction(original_func):
189+ 
190+ @functools.wraps(original_func)
191+ async def async_wrapper(*args, **kwargs):
192+ invocation = _TraceInvocation(args, kwargs)
193+ with _trace_scope(spec, invocation):
194+ invocation.return_value = await next_func(*args, **kwargs)
195+ return invocation.return_value
196+ 
197+ return async_wrapper
198+ 
199+ @functools.wraps(original_func)
200+ def sync_wrapper(*args, **kwargs):
201+ invocation = _TraceInvocation(args, kwargs)
202+ with _trace_scope(spec, invocation):
203+ invocation.return_value = next_func(*args, **kwargs)
204+ return invocation.return_value
205+ 
206+ return sync_wrapper
207+ 
208+ factory.__name__ = "trace_around_{}_{}".format(spec.domain, spec.name).replace(".", "_")
209+ return factory
@@ -36,13 +36,19 @@ def register_service_profiler():
36 if not ok:36 if not ok:
37 return37 return
38 38 
39+ # Trace-only mode is a Python Hook feature and does not register profiling
40+ # callbacks or initialize any C++/torch profiling facility.
41+ if not _vllm_profiler._profiling_requested:
42+ logger.info("VLLM Hook tracing initialized (trace-only mode)")
43+ return
44+ 
39 # 2. 获取回调函数45 # 2. 获取回调函数
40 on_start, on_stop = _vllm_profiler.get_callbacks()46 on_start, on_stop = _vllm_profiler.get_callbacks()
41 47 
42 # 3. 注册回调到 mstx48 # 3. 注册回调到 mstx
43 start_result = mstx_profiler.register_profiler_start_callback(on_start)49 start_result = mstx_profiler.register_profiler_start_callback(on_start)
44 stop_result = mstx_profiler.register_profiler_stop_callback(on_stop)50 stop_result = mstx_profiler.register_profiler_stop_callback(on_stop)
45- 51+ 
46 # 4. 根据结果处理52 # 4. 根据结果处理
47 if start_result.is_dynamic and stop_result.is_dynamic:53 if start_result.is_dynamic and stop_result.is_dynamic:
48 logger.info("Successfully registered VLLM profiler callbacks (dynamic mode)")54 logger.info("Successfully registered VLLM profiler callbacks (dynamic mode)")
@@ -53,6 +59,4 @@ def register_service_profiler():
53 try:59 try:
54 register_torch_profiler()60 register_torch_profiler()
55 except Exception as e:61 except Exception as e:
56- logger.warning(f"[Torch Profiler] Unexpected error in patch_model_runner_with_torch_profiler_register: {e}")62+ logger.warning("[Torch Profiler] Unexpected error in patch_model_runner_with_torch_profiler_register: %s", e)
57- 
58- 
@@ -4,15 +4,25 @@
4 min_version: "0.9.1"4 min_version: "0.9.1"
5 handler: ms_service_profiler.patcher.vllm.handlers.v1.batch_handlers:schedule5 handler: ms_service_profiler.patcher.vllm.handlers.v1.batch_handlers:schedule
6 name: batchFrameworkProcessing6 name: batchFrameworkProcessing
7+ domain: Schedule
8+ trace:
9+ name: vllm.scheduler.schedule
10+ adapter: schedule
7 11 
8- symbol: vllm_ascend.core.scheduler:AscendScheduler.schedule12- symbol: vllm_ascend.core.scheduler:AscendScheduler.schedule
9 min_version: "0.9.1"13 min_version: "0.9.1"
10 handler: ms_service_profiler.patcher.vllm.handlers.v1.batch_handlers:schedule14 handler: ms_service_profiler.patcher.vllm.handlers.v1.batch_handlers:schedule
11 name: batchFrameworkProcessing15 name: batchFrameworkProcessing
16+ domain: Schedule
17+ trace:
18+ name: vllm.scheduler.schedule
19+ adapter: schedule
12 20 
13- symbol: vllm.v1.core.sched.scheduler:Scheduler.add_request21- symbol: vllm.v1.core.sched.scheduler:Scheduler.add_request
14 min_version: "0.9.1"22 min_version: "0.9.1"
15 handler: ms_service_profiler.patcher.vllm.handlers.v1.batch_handlers:add_request23 handler: ms_service_profiler.patcher.vllm.handlers.v1.batch_handlers:add_request
24+ trace:
25+ adapter: request_context
16 26 
17# ===== KV Cache =====27# ===== KV Cache =====
18- symbol: vllm.v1.core.kv_cache_manager:KVCacheManager.free28- symbol: vllm.v1.core.kv_cache_manager:KVCacheManager.free
@@ -38,21 +48,36 @@
38 min_version: "0.9.1"48 min_version: "0.9.1"
39 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model49 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model
40 name: modelExec50 name: modelExec
51+ domain: Execute
52+ trace:
53+ name: vllm.model.execute
54+ adapter: model
41 55 
42- symbol: vllm.v1.executor.multiproc_executor:MultiprocExecutor.execute_model56- symbol: vllm.v1.executor.multiproc_executor:MultiprocExecutor.execute_model
43 min_version: "0.9.1"57 min_version: "0.9.1"
44 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model58 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model
45 name: modelExec59 name: modelExec
60+ domain: Execute
61+ trace:
62+ name: vllm.model.execute
63+ adapter: model
46 64 
47- symbol: vllm.v1.executor.uniproc_executor:UniProcExecutor.execute_model65- symbol: vllm.v1.executor.uniproc_executor:UniProcExecutor.execute_model
48 min_version: "0.9.1"66 min_version: "0.9.1"
49 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model67 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model
50 name: modelExec68 name: modelExec
69+ domain: Execute
70+ trace:
71+ name: vllm.model.execute
72+ adapter: model
51 73 
52- symbol: vllm_ascend.worker.model_runner_v1:NPUModelRunner.execute_model74- symbol: vllm_ascend.worker.model_runner_v1:NPUModelRunner.execute_model
53 name: modelRunnerExec75 name: modelRunnerExec
54 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model_runner76 handler: ms_service_profiler.patcher.vllm.handlers.v1.model_handlers:execute_model_runner
55 domain: Execute77 domain: Execute
78+ trace:
79+ name: vllm_ascend.model_runner.execute
80+ adapter: model
56 81 
57- symbol: vllm_ascend.worker.model_runner_v1:NPUModelRunner._update_states82- symbol: vllm_ascend.worker.model_runner_v1:NPUModelRunner._update_states
58 name: _update_states83 name: _update_states
@@ -88,6 +113,11 @@
88- symbol: vllm.v1.engine.async_llm:AsyncLLM.add_request113- symbol: vllm.v1.engine.async_llm:AsyncLLM.add_request
89 min_version: "0.9.1"114 min_version: "0.9.1"
90 handler: ms_service_profiler.patcher.vllm.handlers.v1.request_handlers:add_request_async115 handler: ms_service_profiler.patcher.vllm.handlers.v1.request_handlers:add_request_async
116+ name: vllm.request
117+ domain: Request
118+ trace:
119+ kind: SERVER
120+ adapter: request
91 121 
92- symbol: vllm.engine.async_llm_engine:AsyncLLMEngine.add_request122- symbol: vllm.engine.async_llm_engine:AsyncLLMEngine.add_request
93 min_version: "0.9.1"123 min_version: "0.9.1"
@@ -97,6 +127,10 @@
97- symbol: vllm.v1.engine.output_processor:OutputProcessor.process_outputs127- symbol: vllm.v1.engine.output_processor:OutputProcessor.process_outputs
98 min_version: "0.9.1"128 min_version: "0.9.1"
99 handler: ms_service_profiler.patcher.vllm.handlers.v1.request_handlers:process_outputs129 handler: ms_service_profiler.patcher.vllm.handlers.v1.request_handlers:process_outputs
130+ name: vllm.output.process
131+ domain: Request
132+ trace:
133+ adapter: output
100 134 
101# ===== Meta =====135# ===== Meta =====
102 136 
@@ -50,6 +50,12 @@ def _is_registry_subprocess() -> bool:
50 return False50 return False
51 51 
52 52 
53+def _clear_hook_trace_runtime() -> None:
54+ from ms_service_profiler.tracer.hook_runtime import get_hook_trace_runtime
55+ 
56+ get_hook_trace_runtime().clear()
57+ 
58+ 
53class VLLMProfiler:59class VLLMProfiler:
54 """vLLM 框架适配器。60 """vLLM 框架适配器。
55 61 
@@ -67,6 +73,9 @@ class VLLMProfiler:
67 self._vllm_version = VLLMProfiler._get_vllm_version()73 self._vllm_version = VLLMProfiler._get_vllm_version()
68 self._controller: Optional[HookController] = None74 self._controller: Optional[HookController] = None
69 self._initialized = False75 self._initialized = False
76+ self._profiling_requested = True
77+ self._profiling_active = True
78+ self._tracing_requested = False
70 79 
71 # -------------------------------------------------------------------------80 # -------------------------------------------------------------------------
72 # 版本检测81 # 版本检测
@@ -184,6 +193,12 @@ class VLLMProfiler:
184 def _load_profiling_config(self) -> Optional[ProfilingConfig]:193 def _load_profiling_config(self) -> Optional[ProfilingConfig]:
185 """加载 profiling 配置文件并返回 ProfilingConfig(concrete + patterns)。"""194 """加载 profiling 配置文件并返回 ProfilingConfig(concrete + patterns)。"""
186 195 
196+ def _load_symbols(path: str) -> ProfilingConfig:
197+ return ConfigLoader(path, self._vllm_version).load_profiling(
198+ enable_profiling=self._profiling_active,
199+ enable_tracing=self._tracing_requested,
200+ )
201+ 
187 def _write_profiling_symbols(env_path: str, default_cfg: str) -> Optional[ProfilingConfig]:202 def _write_profiling_symbols(env_path: str, default_cfg: str) -> Optional[ProfilingConfig]:
188 try:203 try:
189 parent_dir = os.path.dirname(env_path) or '.'204 parent_dir = os.path.dirname(env_path) or '.'
@@ -192,7 +207,7 @@ class VLLMProfiler:
192 dst.write(src.read())207 dst.write(src.read())
193 logger.debug(f"Wrote profiling symbols to env path: {env_path}")208 logger.debug(f"Wrote profiling symbols to env path: {env_path}")
194 logger.info("Loading vLLM profiling symbols from: %s", env_path)209 logger.info("Loading vLLM profiling symbols from: %s", env_path)
195- return ConfigLoader(env_path, self._vllm_version).load_profiling()210+ return _load_symbols(env_path)
196 except Exception as e:211 except Exception as e:
197 logger.warning(f"Failed to write profiling symbols to env path {env_path}: {e}")212 logger.warning(f"Failed to write profiling symbols to env path {env_path}: {e}")
198 return None213 return None
@@ -202,7 +217,7 @@ class VLLMProfiler:
202 if env_path and str(env_path).lower().endswith(('.yaml', '.yml')):217 if env_path and str(env_path).lower().endswith(('.yaml', '.yml')):
203 if os.path.isfile(env_path):218 if os.path.isfile(env_path):
204 logger.info("Loading vLLM profiling symbols from: %s", env_path)219 logger.info("Loading vLLM profiling symbols from: %s", env_path)
205- return ConfigLoader(env_path, self._vllm_version).load_profiling()220+ return _load_symbols(env_path)
206 if default_cfg:221 if default_cfg:
207 return _write_profiling_symbols(env_path, default_cfg)222 return _write_profiling_symbols(env_path, default_cfg)
208 logger.warning("No default config file found to populate PROFILING_SYMBOLS_PATH")223 logger.warning("No default config file found to populate PROFILING_SYMBOLS_PATH")
@@ -211,7 +226,7 @@ class VLLMProfiler:
211 226 
212 if default_cfg:227 if default_cfg:
213 logger.info("Loading vLLM profiling symbols from: %s", default_cfg)228 logger.info("Loading vLLM profiling symbols from: %s", default_cfg)
214- return ConfigLoader(default_cfg, self._vllm_version).load_profiling()229+ return _load_symbols(default_cfg)
215 logger.warning("No config file found")230 logger.warning("No config file found")
216 return None231 return None
217 232 
@@ -258,15 +273,21 @@ class VLLMProfiler:
258 bool: 初始化是否成功273 bool: 初始化是否成功
259 """274 """
260 try:275 try:
261- if not check_profiling_enabled():276+ self._profiling_requested = check_profiling_enabled()
277+ self._profiling_active = self._profiling_requested
278+ self._tracing_requested = os.environ.get("MS_TRACE_ENABLE") == "1"
279+ if not self._profiling_requested and not self._tracing_requested:
262 return False280 return False
263 logger.debug("Initializing VLLM Service Profiler")281 logger.debug("Initializing VLLM Service Profiler")
264 282 
265 # 初始化metrics模块逻辑,后续有metrics独立开关后从此处移出283 # 初始化metrics模块逻辑,后续有metrics独立开关后从此处移出
266- setup_vllm_metrics()284+ if self._profiling_requested:
285+ setup_vllm_metrics()
267 286 
268- # 导入 handlers287+ # Trace-only startup consumes only YAML metadata and must not import
269- self._import_handlers()288+ # or execute the existing business profiling handlers.
289+ if self._profiling_requested:
290+ self._import_handlers()
270 # 创建 SymbolWatchFinder(未加载配置)并安装;registry 子进程内不安装,避免破坏 import 顺序291 # 创建 SymbolWatchFinder(未加载配置)并安装;registry 子进程内不安装,避免破坏 import 顺序
271 watcher = SymbolWatchFinder()292 watcher = SymbolWatchFinder()
272 if not _is_registry_subprocess():293 if not _is_registry_subprocess():
@@ -315,8 +336,12 @@ class VLLMProfiler:
315 """禁用所有 hooks。"""336 """禁用所有 hooks。"""
316 if self._controller is None:337 if self._controller is None:
317 logger.warning("Profiler not initialized, cannot disable hooks")338 logger.warning("Profiler not initialized, cannot disable hooks")
339+ if self._tracing_requested:
340+ _clear_hook_trace_runtime()
318 return341 return
319 self._controller.disable()342 self._controller.disable()
343+ if self._tracing_requested:
344+ _clear_hook_trace_runtime()
320 345 
321 # -------------------------------------------------------------------------346 # -------------------------------------------------------------------------
322 # C++ 回调(委托给 HookController)347 # C++ 回调(委托给 HookController)
@@ -334,7 +359,26 @@ class VLLMProfiler:
334 logger.warning("Profiler not initialized, callback ignored")359 logger.warning("Profiler not initialized, callback ignored")
335 360 
336 return noop, noop361 return noop, noop
337- return self._controller.get_callbacks(self._load_config)362+ if not self._tracing_requested:
363+ return self._controller.get_callbacks(self._load_config)
364+ 
365+ def on_start():
366+ try:
367+ self._profiling_active = self._profiling_requested
368+ profiling, metrics = self._load_config()
369+ self._controller.enable(profiling_handlers=profiling, metrics_handlers=metrics)
370+ except Exception as exc:
371+ logger.exception("Failed to handle profiler start with tracing enabled: %s", exc)
372+ 
373+ def on_stop():
374+ try:
375+ self._profiling_active = False
376+ tracing, metrics = self._load_config()
377+ self._controller.enable(profiling_handlers=tracing, metrics_handlers=metrics)
378+ except Exception as exc:
379+ logger.exception("Failed to keep tracing hooks after profiler stop: %s", exc)
380+ 
381+ return on_start, on_stop
338 382 
339 def get_metric_callbacks(self) -> Tuple[Callable[[], None], Callable[[], None]]:383 def get_metric_callbacks(self) -> Tuple[Callable[[], None], Callable[[], None]]:
340 """返回可注册到 C++ 的 metric 回调函数对(on_start_metric, on_stop_metric)。384 """返回可注册到 C++ 的 metric 回调函数对(on_start_metric, on_stop_metric)。
@@ -11,6 +11,7 @@ import os
11import threading11import threading
12from collections import OrderedDict12from collections import OrderedDict
13from dataclasses import dataclass13from dataclasses import dataclass
14+from itertools import islice
14from typing import Iterable, Optional15from typing import Iterable, Optional
15 16 
16from .otel_hook import HookSpanContext, HookTraceSpan, get_hook_tracer_backend, new_noop_hook_span17from .otel_hook import HookSpanContext, HookTraceSpan, get_hook_tracer_backend, new_noop_hook_span
@@ -46,7 +47,7 @@ class HookTraceRuntime:
46 ) -> HookTraceSpan:47 ) -> HookTraceSpan:
47 if not self.enabled:48 if not self.enabled:
48 return new_noop_hook_span()49 return new_noop_hook_span()
49- normalized_request_ids = [str(item) for item in list(request_ids or [])[:MAX_LINKS_PER_SPAN]]50+ normalized_request_ids = [str(item) for item in islice(request_ids or (), MAX_LINKS_PER_SPAN)]
50 links = self.request_links(normalized_request_ids)51 links = self.request_links(normalized_request_ids)
51 span = self._backend.start_span(name, domain, kind, links=links, start_time_ns=start_time_ns)52 span = self._backend.start_span(name, domain, kind, links=links, start_time_ns=start_time_ns)
52 if span.is_recording:53 if span.is_recording:
@@ -97,7 +98,7 @@ class HookTraceRuntime:
97 with self._lock:98 with self._lock:
98 request_traces = [99 request_traces = [
99 (str(request_id), self._requests.get(str(request_id)))100 (str(request_id), self._requests.get(str(request_id)))
100- for request_id in list(request_ids)[:MAX_LINKS_PER_SPAN]101+ for request_id in islice(request_ids, MAX_LINKS_PER_SPAN)
101 ]102 ]
102 return [103 return [
103 (request_trace.context, request_id)104 (request_trace.context, request_id)
@@ -9,6 +9,7 @@
9 9 
10import os10import os
11import threading11import threading
12+import time
12from dataclasses import dataclass13from dataclasses import dataclass
13from typing import Any, Iterable, Optional, Tuple14from typing import Any, Iterable, Optional, Tuple
14 15 
@@ -19,6 +20,7 @@ from ms_service_profiler.utils.log import logger
19MAX_ATTRIBUTE_COUNT = 3220MAX_ATTRIBUTE_COUNT = 32
20MAX_ATTRIBUTE_KEY_LENGTH = 12821MAX_ATTRIBUTE_KEY_LENGTH = 128
21MAX_ATTRIBUTE_VALUE_LENGTH = 102422MAX_ATTRIBUTE_VALUE_LENGTH = 1024
23+PERFETTO_REGISTRATION_RETRY_INTERVAL_SECONDS = 5.0
22 24 
23 25 
24try:26try:
@@ -150,6 +152,7 @@ class OpenTelemetryHookBackend:
150 def __init__(self):152 def __init__(self):
151 self._lock = threading.RLock()153 self._lock = threading.RLock()
152 self._perfetto_providers = set()154 self._perfetto_providers = set()
155+ self._perfetto_retry_after = {}
153 self._warned_unavailable = False156 self._warned_unavailable = False
154 self._warned_provider_missing = False157 self._warned_provider_missing = False
155 158 
@@ -193,18 +196,25 @@ class OpenTelemetryHookBackend:
193 logger.debug("Failed to probe Perfetto forwarder: %s", exc)196 logger.debug("Failed to probe Perfetto forwarder: %s", exc)
194 perfetto_available = False197 perfetto_available = False
195 if not perfetto_registered and perfetto_available:198 if not perfetto_registered and perfetto_available:
199+ now = time.monotonic()
196 with self._lock:200 with self._lock:
197- if identity not in self._perfetto_providers:201+ retry_after = self._perfetto_retry_after.get(identity, 0.0)
198- processor = PerfettoSpanProcessor()202+ if identity not in self._perfetto_providers and now >= retry_after:
203+ processor = None
199 try:204 try:
205+ processor = PerfettoSpanProcessor()
200 provider.add_span_processor(processor)206 provider.add_span_processor(processor)
201 except Exception as exc:207 except Exception as exc:
202- try:208+ self._perfetto_retry_after[identity] = now + PERFETTO_REGISTRATION_RETRY_INTERVAL_SECONDS
203- processor.shutdown()209+ if processor is not None:
204- except Exception as shutdown_exc:210+ try:
205- logger.debug("Failed to shut down Perfetto span processor: %s", shutdown_exc)211+ processor.shutdown()
212+ except Exception as shutdown_exc:
213+ logger.debug("Failed to shut down Perfetto span processor: %s", shutdown_exc)
206 logger.debug("Failed to register Perfetto span processor: %s", exc)214 logger.debug("Failed to register Perfetto span processor: %s", exc)
207- self._perfetto_providers.add(identity)215+ else:
216+ self._perfetto_providers.add(identity)
217+ self._perfetto_retry_after.pop(identity, None)
208 return provider218 return provider
209 219 
210 @staticmethod220 @staticmethod
@@ -0,0 +1,324 @@
1+# -------------------------------------------------------------------------
2+# This file is part of the MindStudio project.
3+# Copyright (c) 2026 Huawei Technologies Co.,Ltd.
4+#
5+# MindStudio is licensed under Mulan PSL v2.
6+# -------------------------------------------------------------------------
7+ 
8+"""Unit tests for vLLM Hook tracing without importing vLLM handlers."""
9+ 
10+import asyncio
11+from types import SimpleNamespace
12+from unittest.mock import MagicMock, patch
13+ 
14+from ms_service_profiler.patcher.core.config_loader import ConfigLoader
15+from ms_service_profiler.patcher.core.trace_hook import HookTraceSpec, make_trace_around_factory
16+from ms_service_profiler.patcher.vllm import register_service_profiler
17+from ms_service_profiler.patcher.vllm.service_patcher import VLLMProfiler
18+from ms_service_profiler.tracer.hook_runtime import MAX_LINKS_PER_SPAN, HookTraceRuntime
19+from ms_service_profiler.tracer.otel_hook import HookSpanContext
20+ 
21+ 
22+class FakeNativeContext:
23+ is_valid = True
24+ trace_id = int("1" * 32, 16)
25+ span_id = int("2" * 16, 16)
26+ 
27+ 
28+class FakeSpan:
29+ def __init__(self, context=None):
30+ self.context = context or HookSpanContext(FakeNativeContext())
31+ self.attributes = {}
32+ self.end_calls = []
33+ 
34+ @property
35+ def is_recording(self):
36+ return not self.end_calls
37+ 
38+ def set_attribute(self, key, value):
39+ self.attributes[key] = value
40+ 
41+ def activate(self):
42+ return "token"
43+ 
44+ @staticmethod
45+ def deactivate(token):
46+ return None
47+ 
48+ def end(self, success=True, message="", end_time_ns=None):
49+ self.end_calls.append((success, message, end_time_ns))
50+ 
51+ 
52+class FakeBackend:
53+ enabled = True
54+ 
55+ def __init__(self):
56+ self.calls = []
57+ 
58+ @staticmethod
59+ def context_from_headers(headers):
60+ return HookSpanContext(FakeNativeContext()) if headers else HookSpanContext()
61+ 
62+ @staticmethod
63+ def current_context():
64+ return HookSpanContext(FakeNativeContext())
65+ 
66+ def start_span(self, name, domain, kind, **kwargs):
67+ span = FakeSpan()
68+ self.calls.append((name, domain, kind, kwargs, span))
69+ return span
70+ 
71+ def shutdown(self):
72+ return None
73+ 
74+ 
75+def _wrap(spec, original):
76+ return make_trace_around_factory(spec)(original, original)
77+ 
78+ 
79+def test_trace_only_config_does_not_resolve_profiling_handler():
80+ raw = [
81+ {
82+ "symbol": "vllm.v1.engine.async_llm:AsyncLLM.add_request",
83+ "handler": "ms_service_profiler.patcher.vllm.handlers.v1.request_handlers:add_request_async",
84+ "trace": {"adapter": "request", "kind": "SERVER"},
85+ }
86+ ]
87+ with (
88+ patch("ms_service_profiler.patcher.core.config_loader.load_yaml_config", return_value=raw),
89+ patch("ms_service_profiler.patcher.core.config_loader._resolve_handler_func") as resolve_handler,
90+ ):
91+ config = ConfigLoader("unused.yaml", "0.15.0").load_profiling(
92+ enable_profiling=False,
93+ enable_tracing=True,
94+ )
95+ 
96+ hooker = config.concrete[raw[0]["symbol"]][0]
97+ resolve_handler.assert_not_called()
98+ assert hooker.need_locals is False
99+ assert hooker.context_hook_funcs == []
100+ assert hooker.around_hook_factory is not None
101+ 
102+ 
103+def test_builtin_yaml_supplies_trace_hooks_without_vllm_ascend_provider():
104+ profiler = VLLMProfiler()
105+ profiler._profiling_active = False
106+ profiler._tracing_requested = True
107+ 
108+ config = profiler._load_profiling_config()
109+ 
110+ symbols = set(config.concrete)
111+ assert "vllm.v1.engine.async_llm:AsyncLLM.add_request" in symbols
112+ assert "vllm_ascend.core.scheduler:AscendScheduler.schedule" in symbols
113+ assert "vllm.engine.async_llm_engine:AsyncLLMEngine.add_request" not in symbols
114+ assert all(hooker.around_hook_factory is not None for hookers in config.concrete.values() for hooker in hookers)
115+ 
116+ 
117+def test_trace_only_plugin_registration_does_not_start_profiling_facilities():
118+ profiler = MagicMock()
119+ profiler.initialize.return_value = True
120+ profiler._profiling_requested = False
121+ profiler.get_callbacks.return_value = (MagicMock(), MagicMock())
122+ dynamic_result = SimpleNamespace(is_dynamic=True)
123+ with (
124+ patch("ms_service_profiler.patcher.vllm._vllm_profiler", profiler),
125+ patch(
126+ "ms_service_profiler.patcher.vllm.mstx_profiler.register_profiler_start_callback",
127+ return_value=dynamic_result,
128+ ) as start_callback,
129+ patch(
130+ "ms_service_profiler.patcher.vllm.mstx_profiler.register_profiler_stop_callback",
131+ return_value=dynamic_result,
132+ ) as stop_callback,
133+ patch("ms_service_profiler.patcher.vllm.register_torch_profiler") as torch_profiler,
134+ ):
135+ register_service_profiler()
136+ 
137+ start_callback.assert_not_called()
138+ stop_callback.assert_not_called()
139+ torch_profiler.assert_not_called()
140+ 
141+ 
142+def test_trace_only_initialize_skips_metrics_and_profiling_handlers():
143+ profiler = VLLMProfiler()
144+ with (
145+ patch.dict("os.environ", {"MS_TRACE_ENABLE": "1"}, clear=True),
146+ patch("ms_service_profiler.patcher.vllm.service_patcher.check_profiling_enabled", return_value=False),
147+ patch("ms_service_profiler.patcher.vllm.service_patcher.setup_vllm_metrics") as setup_metrics,
148+ patch.object(profiler, "_import_handlers") as import_handlers,
149+ patch.object(profiler, "enable_hooks") as enable_hooks,
150+ patch("ms_service_profiler.patcher.vllm.service_patcher.install_symbol_watcher", return_value=True),
151+ ):
152+ assert profiler.initialize() is True
153+ 
154+ assert profiler._tracing_requested is True
155+ setup_metrics.assert_not_called()
156+ import_handlers.assert_not_called()
157+ enable_hooks.assert_called_once()
158+ 
159+ 
160+def test_profiling_stop_keeps_trace_only_hooks_enabled():
161+ profiler = VLLMProfiler()
162+ profiler._profiling_requested = True
163+ profiler._profiling_active = True
164+ profiler._tracing_requested = True
165+ profiler._controller = MagicMock()
166+ trace_config = MagicMock()
167+ with patch.object(profiler, "_load_config", return_value=(trace_config, None)):
168+ _, on_stop = profiler.get_callbacks()
169+ on_stop()
170+ 
171+ assert profiler._profiling_active is False
172+ profiler._controller.enable.assert_called_once_with(
173+ profiling_handlers=trace_config,
174+ metrics_handlers=None,
175+ )
176+ profiler._controller.disable.assert_not_called()
177+ 
178+ 
179+def test_profiling_only_keeps_original_controller_callbacks():
180+ profiler = VLLMProfiler()
181+ profiler._tracing_requested = False
182+ profiler._controller = MagicMock()
183+ expected = (MagicMock(), MagicMock())
184+ profiler._controller.get_callbacks.return_value = expected
185+ 
186+ assert profiler.get_callbacks() == expected
187+ profiler._controller.get_callbacks.assert_called_once_with(profiler._load_config)
188+ 
189+ 
190+def test_combined_config_uses_one_hooker_with_profiling_and_trace():
191+ profiling_handler = MagicMock()
192+ raw = [{"symbol": "vllm.module:Engine.run", "handler": "allowed:handler", "trace": True}]
193+ with (
194+ patch("ms_service_profiler.patcher.core.config_loader.load_yaml_config", return_value=raw),
195+ patch("ms_service_profiler.patcher.core.config_loader._resolve_handler_func", return_value=profiling_handler),
196+ ):
197+ config = ConfigLoader("unused.yaml").load_profiling(enable_profiling=True, enable_tracing=True)
198+ 
199+ hookers = config.concrete[raw[0]["symbol"]]
200+ assert len(hookers) == 1
201+ assert hookers[0].wrap_hook_func is profiling_handler
202+ assert hookers[0].context_hook_funcs == []
203+ assert hookers[0].around_hook_factory is not None
204+ 
205+ 
206+def test_duplicate_trace_for_one_symbol_keeps_one_effective_hook():
207+ raw = [
208+ {"symbol": "vllm.module:Engine.run", "trace": {"name": "first"}},
209+ {"symbol": "vllm.module:Engine.run", "trace": {"name": "second"}},
210+ ]
211+ with patch("ms_service_profiler.patcher.core.config_loader.load_yaml_config", return_value=raw):
212+ config = ConfigLoader("unused.yaml").load_profiling(enable_profiling=False, enable_tracing=True)
213+ 
214+ assert len(config.concrete[raw[0]["symbol"]]) == 1
215+ 
216+ 
217+def test_runtime_registers_existing_context_and_links_child_span():
218+ backend = FakeBackend()
219+ runtime = HookTraceRuntime(backend)
220+ 
221+ assert runtime.start_request("request-1", {"traceparent": "valid"}) is True
222+ schedule_span = runtime.start_span(
223+ "vllm.scheduler.schedule",
224+ "Schedule",
225+ request_ids=["request-1", "request-without-local-state"],
226+ )
227+ 
228+ links = backend.calls[0][3]["links"]
229+ assert [request_id for _, request_id in links] == ["request-1"]
230+ assert runtime.finish_request("request-1") is True
231+ assert schedule_span.is_recording
232+ 
233+ 
234+def test_runtime_limits_iterable_consumption_before_materializing_request_ids():
235+ backend = FakeBackend()
236+ runtime = HookTraceRuntime(backend)
237+ start_consumed = []
238+ link_consumed = []
239+ 
240+ def request_ids(consumed):
241+ for index in range(MAX_LINKS_PER_SPAN + 10):
242+ consumed.append(index)
243+ yield "request-{}".format(index)
244+ 
245+ span = runtime.start_span("schedule", "Schedule", request_ids=request_ids(start_consumed))
246+ runtime.request_links(request_ids(link_consumed))
247+ 
248+ assert len(start_consumed) == MAX_LINKS_PER_SPAN
249+ assert len(link_consumed) == MAX_LINKS_PER_SPAN
250+ assert len(span.attributes["request.ids"]) == MAX_LINKS_PER_SPAN
251+ 
252+ 
253+def test_schedule_adapter_adds_request_links_and_batch_attributes():
254+ runtime = MagicMock(enabled=True)
255+ span = FakeSpan()
256+ runtime.start_span.return_value = span
257+ scheduler_output = SimpleNamespace(
258+ num_scheduled_tokens={"request-1": 4, "request-2": 2},
259+ total_num_scheduled_tokens=6,
260+ )
261+ 
262+ def original():
263+ return scheduler_output
264+ 
265+ with (
266+ patch("ms_service_profiler.patcher.core.trace_hook.get_hook_trace_runtime", return_value=runtime),
267+ patch("ms_service_profiler.patcher.core.trace_hook.time.time_ns", side_effect=[100, 200]),
268+ ):
269+ assert _wrap(HookTraceSpec("schedule", "Schedule", adapter="schedule"), original)() is scheduler_output
270+ 
271+ runtime.start_span.assert_called_once_with(
272+ "schedule", "Schedule", "INTERNAL", request_ids=["request-1", "request-2"], start_time_ns=100
273+ )
274+ assert span.attributes == {"batch.request_count": 2, "batch.scheduled_tokens": 6}
275+ assert span.end_calls == [(True, "", 200)]
276+ 
277+ 
278+def test_request_context_adapter_registers_w3c_headers_without_creating_span():
279+ runtime = MagicMock(enabled=True)
280+ request = SimpleNamespace(request_id="request-1", trace_headers={"traceparent": "valid"})
281+ 
282+ def original(_, req):
283+ return req.request_id
284+ 
285+ with patch("ms_service_profiler.patcher.core.trace_hook.get_hook_trace_runtime", return_value=runtime):
286+ result = _wrap(HookTraceSpec("add_request", "Request", adapter="request_context"), original)(object(), request)
287+ 
288+ assert result == "request-1"
289+ runtime.register_request_context.assert_called_once_with("request-1", {"traceparent": "valid"})
290+ runtime.start_span.assert_not_called()
291+ 
292+ 
293+def test_output_adapter_finishes_only_completed_requests():
294+ runtime = MagicMock(enabled=True)
295+ span = FakeSpan()
296+ runtime.start_span.return_value = span
297+ outputs = [
298+ SimpleNamespace(request_id="running", finish_reason=None),
299+ SimpleNamespace(request_id="finished", finish_reason="stop"),
300+ ]
301+ 
302+ def original(_, result):
303+ return result
304+ 
305+ with patch("ms_service_profiler.patcher.core.trace_hook.get_hook_trace_runtime", return_value=runtime):
306+ assert _wrap(HookTraceSpec("output", "Request", adapter="output"), original)(object(), outputs) is outputs
307+ 
308+ runtime.finish_request.assert_called_once_with("finished", True)
309+ assert span.end_calls == [(True, "", None)]
310+ 
311+ 
312+def test_async_around_hook_preserves_awaitable_result():
313+ runtime = MagicMock(enabled=True)
314+ span = FakeSpan()
315+ runtime.start_span.return_value = span
316+ 
317+ async def original(value):
318+ return value + 1
319+ 
320+ with patch("ms_service_profiler.patcher.core.trace_hook.get_hook_trace_runtime", return_value=runtime):
321+ result = asyncio.run(_wrap(HookTraceSpec("execute", "Execute"), original)(1))
322+ 
323+ assert result == 2
324+ assert span.end_calls == [(True, "", None)]
@@ -100,6 +100,34 @@ def test_perfetto_processor_registration_failure_does_not_disable_jaeger_provide
100 assert backend._get_provider() is provider100 assert backend._get_provider() is provider
101 101 
102 processor.shutdown.assert_called_once()102 processor.shutdown.assert_called_once()
103+ assert id(provider) not in backend._perfetto_providers
104+ 
105+ 
106+def test_perfetto_processor_registration_retries_after_backoff():
107+ backend = OpenTelemetryHookBackend()
108+ provider = MagicMock()
109+ provider.add_span_processor.side_effect = [RuntimeError("registration failed"), None]
110+ first_processor = MagicMock()
111+ second_processor = MagicMock()
112+ 
113+ with (
114+ patch.dict("os.environ", {"MS_TRACE_ENABLE": "1"}, clear=True),
115+ patch.object(backend, "_active_global_provider", return_value=provider),
116+ patch("ms_service_profiler.tracer.otel_hook.PerfettoSocketSender.is_available", return_value=True),
117+ patch(
118+ "ms_service_profiler.tracer.otel_hook.PerfettoSpanProcessor",
119+ side_effect=[first_processor, second_processor],
120+ ),
121+ patch("ms_service_profiler.tracer.otel_hook.time.monotonic", side_effect=[100.0, 101.0, 106.0]),
122+ ):
123+ assert backend._get_provider() is provider
124+ assert backend._get_provider() is provider
125+ assert backend._get_provider() is provider
126+ 
127+ assert provider.add_span_processor.call_count == 2
128+ first_processor.shutdown.assert_called_once()
129+ second_processor.shutdown.assert_not_called()
130+ assert id(provider) in backend._perfetto_providers
103 131 
104 132 
105def test_backend_missing_otel_dependency_is_fail_open():133def test_backend_missing_otel_dependency_is_fail_open():
@@ -234,6 +234,63 @@ def test_vllmhookerbase_do_hook_given_hook_points_when_applying_then_functions_r
234 hooker.hooks[0].recover()234 hooker.hooks[0].recover()
235 235 
236 236 
237+def test_vllmhookerbase_do_hook_given_around_hook_when_calling_then_wraps_target(cleanup_hook_registry):
238+ """The optional tracing layer surrounds the existing callable."""
239+ events = []
240+ 
241+ def around_factory(next_func, original_func):
242+ def wrapper(*args, **kwargs):
243+ events.append(("enter", args))
244+ result = next_func(*args, **kwargs)
245+ events.append(("exit", result))
246+ return result
247+ 
248+ return wrapper
249+ 
250+ class AroundHooker(VLLMHookerBase):
251+ def init(self):
252+ self.around_hook_factory = around_factory
253+ self.do_hook([sample_function], lambda ori_func: ori_func)
254+ 
255+ hooker = AroundHooker()
256+ hooker.init()
257+ try:
258+ assert sample_function() == "original function"
259+ assert events == [("enter", ()), ("exit", "original function")]
260+ finally:
261+ hooker.hooks[0].recover()
262+ 
263+ 
264+def test_vllmhookerbase_around_hook_does_not_retry_business_exception(cleanup_hook_registry):
265+ calls = []
266+ expected = RuntimeError("business failed")
267+ 
268+ def business_function():
269+ calls.append("called")
270+ raise expected
271+ 
272+ def around_factory(next_func, original_func):
273+ def wrapper(*args, **kwargs):
274+ return next_func(*args, **kwargs)
275+ 
276+ return wrapper
277+ 
278+ class ExceptionHooker(VLLMHookerBase):
279+ def init(self):
280+ return None
281+ 
282+ hooker = ExceptionHooker()
283+ hooker.around_hook_factory = around_factory
284+ hooker.do_hook([business_function], lambda ori_func: ori_func)
285+ try:
286+ with pytest.raises(RuntimeError) as error:
287+ hooker.hooks[0].new_function()
288+ assert error.value is expected
289+ assert calls == ["called"]
290+ finally:
291+ hooker.hooks[0].recover()
292+ 
293+ 
237# Test cases for vllm_hook decorator294# Test cases for vllm_hook decorator
238@patcher(295@patcher(
239 hook_points=[("patcher.core.module_hook", "sample_function")],296 hook_points=[("patcher.core.module_hook", "sample_function")],