Skip to content

Commit fa0f9e9

Browse files
committed
Reject feedforward outputs at earlier layers
Add validation for feedforward graphs so all inferred terminal output nodes must be located at the final layer depth. This prevents artifacts from listing earlier sink nodes that EdgeModel.forward() would not return, keeping inferred outputs consistent with the model output tensor. Add regression coverage for feedforward DAGs with terminal nodes at multiple depths.
1 parent ccadcf9 commit fa0f9e9

6 files changed

Lines changed: 104 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ feature_attributions = e2t.interpret_model(
129129
artifact=artifact,
130130
data=data,
131131
target="features",
132-
method="integrated_gradients",
132+
method="IntegratedGradients",
133133
)
134134
```
135135

src/edge2torch/compile/execution_plan.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,27 @@ def build_feedforward_execution_plan(
141141
node for node in graph.nodes if len(children[node]) == 0
142142
)
143143

144+
output_node_depths = {
145+
node: node_to_depth[node] for node in output_node_names
146+
}
147+
148+
if output_node_depths:
149+
final_output_depth = max(output_node_depths.values())
150+
early_output_nodes = sorted(
151+
node
152+
for node, node_depth in output_node_depths.items()
153+
if node_depth != final_output_depth
154+
)
155+
156+
if early_output_nodes:
157+
early_output_str = ", ".join(early_output_nodes)
158+
raise Edge2TorchError(
159+
"Feedforward compilation requires all terminal output nodes "
160+
"to be at the same layer depth. Output node(s) at earlier "
161+
"depth than the final output layer are not supported: "
162+
f"{early_output_str}."
163+
)
164+
144165
has_initial_weight = "initial_weight" in original_edges.columns
145166
has_constraint = "constraint" in original_edges.columns
146167

src/edge2torch/graph/validate.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,26 +248,54 @@ def _validate_feedforward_graph(
248248
return
249249

250250
visited_nodes: set[str] = set()
251+
node_to_depth: dict[str, int] = {}
252+
depth = 0
251253

252254
while current_layer_nodes:
253255
next_layer_candidates: set[str] = set()
254256

255257
for node in current_layer_nodes:
256258
visited_nodes.add(node)
259+
node_to_depth[node] = depth
257260

258261
for child in children[node]:
259262
in_degree[child] -= 1
260263

261264
if in_degree[child] == 0:
262265
next_layer_candidates.add(child)
263266

267+
depth += 1
264268
current_layer_nodes = sorted(next_layer_candidates)
265269

266270
if len(visited_nodes) != len(node_names):
267271
report.errors.append(
268272
"Feedforward compilation requires an acyclic, layerable graph. "
269273
"The graph contains at least one cycle or unresolved dependency."
270274
)
275+
return
276+
277+
output_node_depths = {
278+
node: node_to_depth[node]
279+
for node in node_names
280+
if len(children[node]) == 0
281+
}
282+
283+
if output_node_depths:
284+
final_output_depth = max(output_node_depths.values())
285+
early_output_nodes = sorted(
286+
node
287+
for node, node_depth in output_node_depths.items()
288+
if node_depth != final_output_depth
289+
)
290+
291+
if early_output_nodes:
292+
early_output_str = ", ".join(early_output_nodes)
293+
report.errors.append(
294+
"Feedforward compilation requires all terminal output nodes "
295+
"to be at the same layer depth. Output node(s) at earlier "
296+
"depth than the final output layer are not supported: "
297+
f"{early_output_str}."
298+
)
271299

272300

273301
def _validate_recurrent_graph(

tests/api/compile_graph/test_api_compile_graph.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@
99
from edge2torch.utils.errors import Edge2TorchError
1010

1111

12+
def test_compile_graph_rejects_feedforward_terminal_outputs_at_multi_depths():
13+
edgelist = pd.DataFrame(
14+
{
15+
"source": ["input", "input", "hidden"],
16+
"target": ["early_output", "hidden", "late_output"],
17+
}
18+
)
19+
20+
with pytest.raises(
21+
Edge2TorchError,
22+
match="terminal output nodes.*same layer depth",
23+
):
24+
compile_graph(
25+
edgelist=edgelist,
26+
backend="feedforward",
27+
quiet=True,
28+
)
29+
30+
1231
def test_compile_graph_returns_model_and_artifact_for_valid_feedforward_graph():
1332
edgelist = pd.DataFrame(
1433
{

tests/module/compile/test_execution_plan_internal.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ def __init__(self, edges, nodes=None):
2323
# build_feedforward_execution_plan --------------------------------------------
2424

2525

26+
def test_build_feedforward_execution_plan_rejects_terminal_out_multi_depths():
27+
graph = _Graph(
28+
[
29+
("input", "early_output"),
30+
("input", "hidden"),
31+
("hidden", "late_output"),
32+
]
33+
)
34+
35+
with pytest.raises(
36+
Edge2TorchError,
37+
match="terminal output nodes.*same layer depth",
38+
):
39+
build_feedforward_execution_plan(graph)
40+
41+
2642
def test_build_feedforward_execution_plan_rejects_no_input_nodes():
2743
graph = _Graph(
2844
[

tests/module/graph/test_validate_internal.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,25 @@ def test_validate_common_graph_structure_errors_for_duplicate_edges():
122122
assert report.notes == ["Graph contains 2 node(s) and 2 edge(s)."]
123123

124124

125+
def test_validate_feedforward_graph_rejects_terminal_outputs_at_multi_depths():
126+
graph = _Graph(
127+
[
128+
("input", "early_output"),
129+
("input", "hidden"),
130+
("hidden", "late_output"),
131+
]
132+
)
133+
report = ValidationReport()
134+
135+
_validate_feedforward_graph(graph=graph, report=report)
136+
137+
assert any(
138+
"all terminal output nodes to be at the same layer depth" in error
139+
and "early_output" in error
140+
for error in report.errors
141+
)
142+
143+
125144
def test_validate_feedforward_graph_accepts_layerable_graph():
126145
graph = _Graph(
127146
[

0 commit comments

Comments
 (0)