Auto annotate context in dumper (#19071)
This commit is contained in:
@@ -481,6 +481,7 @@ class _Dumper:
|
|||||||
class _NonIntrusiveDumper:
|
class _NonIntrusiveDumper:
|
||||||
_NAME_PREFIX = "non_intrusive__"
|
_NAME_PREFIX = "non_intrusive__"
|
||||||
_CORE_FIELDS: frozenset[str] = frozenset({"input_ids", "positions"})
|
_CORE_FIELDS: frozenset[str] = frozenset({"input_ids", "positions"})
|
||||||
|
_LAYER_NAME_RE = re.compile(r"(?:.+\.)?layers\.(\d+)$")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -492,6 +493,9 @@ class _NonIntrusiveDumper:
|
|||||||
self._mode = mode
|
self._mode = mode
|
||||||
|
|
||||||
for module_name, module in model.named_modules():
|
for module_name, module in model.named_modules():
|
||||||
|
if ctx := self._detect_module_ctx(module_name, module):
|
||||||
|
self._register_ctx_hooks(module, ctx=ctx)
|
||||||
|
|
||||||
module.register_forward_hook(
|
module.register_forward_hook(
|
||||||
self._make_forward_hook(
|
self._make_forward_hook(
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
@@ -499,6 +503,30 @@ class _NonIntrusiveDumper:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _detect_module_ctx(
|
||||||
|
cls, module_name: str, module: "torch.nn.Module"
|
||||||
|
) -> Optional[dict]:
|
||||||
|
if cls._LAYER_NAME_RE.fullmatch(module_name):
|
||||||
|
# Megatron
|
||||||
|
if hasattr(module, "layer_number"):
|
||||||
|
return {"layer_id": module.layer_number - 1}
|
||||||
|
# SGLang
|
||||||
|
if hasattr(module, "layer_id"):
|
||||||
|
return {"layer_id": module.layer_id}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _register_ctx_hooks(self, module: "torch.nn.Module", *, ctx: dict) -> None:
|
||||||
|
clear_ctx = {k: None for k in ctx}
|
||||||
|
module.register_forward_pre_hook(
|
||||||
|
lambda _mod, _input, _ctx=ctx: self._dumper.set_ctx(**_ctx)
|
||||||
|
)
|
||||||
|
module.register_forward_hook(
|
||||||
|
lambda _mod, _input, _output, _clear=clear_ctx: self._dumper.set_ctx(
|
||||||
|
**_clear
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _make_forward_hook(self, *, module_name: str, is_root: bool):
|
def _make_forward_hook(self, *, module_name: str, is_root: bool):
|
||||||
def _hook(_module, input, output):
|
def _hook(_module, input, output):
|
||||||
for i, item in enumerate(input):
|
for i, item in enumerate(input):
|
||||||
|
|||||||
@@ -1370,5 +1370,146 @@ class TestNonIntrusiveDumperConfigMode(_NonIntrusiveTestBase):
|
|||||||
assert "non_intrusive__layer.output" in captured
|
assert "non_intrusive__layer.output" in captured
|
||||||
|
|
||||||
|
|
||||||
|
class TestNonIntrusiveLayerIdCtx(_NonIntrusiveTestBase):
|
||||||
|
"""Tests for automatic layer_id context injection via set_ctx."""
|
||||||
|
|
||||||
|
def test_layer_id_from_layer_number(self, tmp_path):
|
||||||
|
"""Megatron PP: layer_number (1-based global) -> layer_id = layer_number - 1."""
|
||||||
|
|
||||||
|
class Layer(torch.nn.Module):
|
||||||
|
def __init__(self, layer_number: int):
|
||||||
|
super().__init__()
|
||||||
|
self.layer_number = layer_number
|
||||||
|
self.linear = torch.nn.Linear(4, 4)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
return self.linear(x)
|
||||||
|
|
||||||
|
class Inner(torch.nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.layers = torch.nn.ModuleList([Layer(10), Layer(11)])
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
for layer in self.layers:
|
||||||
|
x = layer(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
d = self._make_dumper(tmp_path)
|
||||||
|
model = self._wrap_as_outer(Inner)
|
||||||
|
d.register_non_intrusive_dumper(model)
|
||||||
|
|
||||||
|
x = torch.randn(2, 4)
|
||||||
|
with d.capture_output() as captured:
|
||||||
|
model(x)
|
||||||
|
|
||||||
|
layer0_key = "non_intrusive__model.layers.0.linear.output"
|
||||||
|
layer1_key = "non_intrusive__model.layers.1.linear.output"
|
||||||
|
assert layer0_key in captured
|
||||||
|
assert layer1_key in captured
|
||||||
|
assert captured[layer0_key]["meta"]["layer_id"] == 9
|
||||||
|
assert captured[layer1_key]["meta"]["layer_id"] == 10
|
||||||
|
|
||||||
|
root_key = "non_intrusive__output"
|
||||||
|
assert root_key in captured
|
||||||
|
assert "layer_id" not in captured[root_key]["meta"]
|
||||||
|
|
||||||
|
def test_layer_id_from_layer_id_attr(self, tmp_path):
|
||||||
|
"""SGLang style: module has layer_id attribute directly."""
|
||||||
|
|
||||||
|
class Layer(torch.nn.Module):
|
||||||
|
def __init__(self, layer_id: int):
|
||||||
|
super().__init__()
|
||||||
|
self.layer_id = layer_id
|
||||||
|
self.linear = torch.nn.Linear(4, 4)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
return self.linear(x)
|
||||||
|
|
||||||
|
class Inner(torch.nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.layers = torch.nn.ModuleList([Layer(5)])
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
for layer in self.layers:
|
||||||
|
x = layer(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
d = self._make_dumper(tmp_path)
|
||||||
|
model = self._wrap_as_outer(Inner)
|
||||||
|
d.register_non_intrusive_dumper(model)
|
||||||
|
|
||||||
|
x = torch.randn(2, 4)
|
||||||
|
with d.capture_output() as captured:
|
||||||
|
model(x)
|
||||||
|
|
||||||
|
layer_key = "non_intrusive__model.layers.0.linear.output"
|
||||||
|
assert layer_key in captured
|
||||||
|
assert captured[layer_key]["meta"]["layer_id"] == 5
|
||||||
|
|
||||||
|
def test_no_layer_id_when_no_attr(self, tmp_path):
|
||||||
|
"""layers.N modules without layer_number/layer_id -> no layer_id injected."""
|
||||||
|
|
||||||
|
class Inner(torch.nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.layers = torch.nn.ModuleList(
|
||||||
|
[torch.nn.Linear(4, 4), torch.nn.Linear(4, 4)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
for layer in self.layers:
|
||||||
|
x = layer(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
d = self._make_dumper(tmp_path)
|
||||||
|
model = self._wrap_as_outer(Inner)
|
||||||
|
d.register_non_intrusive_dumper(model)
|
||||||
|
|
||||||
|
x = torch.randn(2, 4)
|
||||||
|
with d.capture_output() as captured:
|
||||||
|
model(x)
|
||||||
|
|
||||||
|
assert len(captured) > 0
|
||||||
|
for key, entry in captured.items():
|
||||||
|
assert "layer_id" not in entry["meta"], f"{key} has unexpected layer_id"
|
||||||
|
|
||||||
|
def test_filter_by_layer_id(self, tmp_path):
|
||||||
|
"""filter='layer_id=0' keeps only layer 0 dumps."""
|
||||||
|
|
||||||
|
class Layer(torch.nn.Module):
|
||||||
|
def __init__(self, layer_number: int):
|
||||||
|
super().__init__()
|
||||||
|
self.layer_number = layer_number
|
||||||
|
self.linear = torch.nn.Linear(4, 4)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
return self.linear(x)
|
||||||
|
|
||||||
|
class Inner(torch.nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.layers = torch.nn.ModuleList([Layer(1), Layer(2)])
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
for layer in self.layers:
|
||||||
|
x = layer(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
d = self._make_dumper(tmp_path, filter="layer_id=0")
|
||||||
|
model = self._wrap_as_outer(Inner)
|
||||||
|
d.register_non_intrusive_dumper(model)
|
||||||
|
|
||||||
|
x = torch.randn(2, 4)
|
||||||
|
with d.capture_output() as captured:
|
||||||
|
model(x)
|
||||||
|
|
||||||
|
layer0_keys = [k for k in captured if "layers.0" in k]
|
||||||
|
layer1_keys = [k for k in captured if "layers.1" in k]
|
||||||
|
assert len(layer0_keys) > 0, "layer 0 dumps should be kept"
|
||||||
|
assert len(layer1_keys) == 0, f"layer 1 dumps should be filtered: {layer1_keys}"
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(pytest.main([__file__]))
|
sys.exit(pytest.main([__file__]))
|
||||||
|
|||||||
Reference in New Issue
Block a user