Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions simpler_setup/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,16 @@ python -m simpler_setup.tools.deps_viewer outputs/<case>_<ts>/deps.json \
python -m simpler_setup.tools.deps_viewer outputs/<case>_<ts>/deps.json \
--func-names outputs/<case>_<ts>/name_map_TestPA_basic.json

# Transitive reduction: drop edges implied by a longer path, print what was removed
# Transitive reduction: select non-redundant edges, print what was removed
python -m simpler_setup.tools.deps_viewer outputs/<case>_<ts>/deps.json \
--edge-mode reduced

# Redundant-only: draw ONLY the transitively-implied edges reduced would drop
# Redundant-only: select the transitively-implied edges reduced would drop
python -m simpler_setup.tools.deps_viewer outputs/<case>_<ts>/deps.json \
--edge-mode omitted
```

`--edge-mode` selects which structural `(pred, succ)` edges are drawn:
`--edge-mode` selects which structural `(pred, succ)` edges are visible:

- `full` (default) — every dependency edge.
- `reduced` — the minimal (transitively-reduced) edge set: every edge already
Expand All @@ -323,21 +323,24 @@ python -m simpler_setup.tools.deps_viewer outputs/<case>_<ts>/deps.json \
`reduced` and `omitted` print the redundant edges to stdout as a
`<task> -> <task>` list, where each task uses the same label as the rendered
graph — the bare `local` counter when every task is in ring 0, or the explicit
`(ring, local)` tuple once any task lives in ring >= 1. Both `text` and `html`
output honor the mode. When `-o` is omitted the graph is written to a
mode-specific stem (`deps_viewer_reduced.*` / `deps_viewer_omitted.*`) rather
than `deps_viewer.*` so it never clobbers a full-graph render in the same
directory. Reduction is purely structural (it ignores the per-edge tensor/arg
identity) and is skipped with a warning if the graph contains a cycle.
`(ring, local)` tuple once any task lives in ring >= 1. Text output emits only
the selected edge set. HTML output keeps every edge in the Graphviz layout and
colors unselected edges like the page background, so `reduced` / `omitted`
preserve the full-graph node placement and routing while showing only the
Comment thread
indigo1973 marked this conversation as resolved.
selected edge set. When `-o` is omitted the graph is written to a mode-specific
stem (`deps_viewer_reduced.*` / `deps_viewer_omitted.*`) rather than
`deps_viewer.*` so it never clobbers a full-graph render in the same directory.
Reduction is purely structural (it ignores the per-edge tensor/arg identity)
and is skipped with a warning if the graph contains a cycle.

### Command-Line Options

| Option | Short | Description |
| ------ | ----- | ----------- |
| `input` | | Path to `deps.json` (default: newest under `./outputs/`) |
| `--output` | `-o` | Output path (default: `deps_viewer.txt` for text, `deps_viewer.html` for HTML; `reduced`/`omitted` use the `deps_viewer_reduced.*` / `deps_viewer_omitted.*` stem) |
| `--output` | `-o` | Output path; default stem is `deps_viewer`, or `deps_viewer_{mode}` for `reduced` / `omitted` |
| `--format` | | Output format: `text` (default) or `html` |
| `--edge-mode` | | `full` (default) draws every edge; `reduced` draws the transitively-reduced (minimal) set; `omitted` draws only the redundant edges reduced would drop. `reduced`/`omitted` print the redundant edges and apply to both formats. |
| `--edge-mode` | | Select visible edges: `full`, `reduced`, or `omitted`; HTML preserves full layout. |
| `--engine` | | HTML-only Graphviz layout engine: `dot` (default), `sfdp`, `neato`, `fdp`, `circo`, `twopi` |
| `--direction` | | HTML-only flow direction for hierarchical layouts: `LR` (default) / `TB` / `BT` / `RL` |
| `--show-tensor-info` | | HTML-only: render per-task tensor rows and route edges to specific arg ports |
Expand Down
85 changes: 64 additions & 21 deletions simpler_setup/tools/deps_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
python -m simpler_setup.tools.deps_viewer DEPS_JSON --edge-mode reduced
python -m simpler_setup.tools.deps_viewer DEPS_JSON --edge-mode omitted

``--edge-mode`` selects which edges are drawn (structural transitive reduction,
``--edge-mode`` selects which edges are visible (structural transitive reduction,
purely on ``(pred, succ)`` — per-edge tensor identity is ignored, and it is
skipped with a warning if the graph contains a cycle):

Expand All @@ -44,8 +44,10 @@
- ``omitted`` — only the redundant edges ``reduced`` would drop (its complement),
useful for auditing exactly which dependencies are transitively covered.

``reduced`` and ``omitted`` print the redundant edges to stdout and emit in
whichever format is selected.
``reduced`` and ``omitted`` print the redundant edges to stdout. Text output
emits only the selected edge set. HTML output keeps every edge in the Graphviz
layout and colors the unselected edges like the page background, preserving the
full-graph layout while making the selected edge set visible.
Comment thread
indigo1973 marked this conversation as resolved.

HTML output requires Graphviz installed (``brew install graphviz`` /
``apt install graphviz``). Text output does not.
Expand Down Expand Up @@ -733,6 +735,7 @@ def emit_dot(
tensor_table=None,
task_table=None,
show_tensor_info=None,
hidden_edges=None,
):
"""Graphviz DOT source. Used internally to feed the layout engine before
wrapping the SVG in HTML.
Expand All @@ -752,6 +755,7 @@ def emit_dot(
annotations = annotations or {}
tensor_table = tensor_table or {}
task_table = task_table or {}
hidden_edges = set(hidden_edges or ())
show_tensor = bool(task_table) if show_tensor_info is None else bool(show_tensor_info and task_table)
lines = [
"digraph deps {",
Expand All @@ -767,13 +771,22 @@ def emit_dot(
lines.append(f" {_node_id(n)} [shape=none, margin=0, label=<{html}>];")
continue
lines.append(f" {_node_id(n)} [{_plain_node_attrs(n, meta, task_table, fmt_task)}];")

def edge_attr_str(edge_attrs):
return (" [" + ", ".join(edge_attrs) + "]") if edge_attrs else ""

def hidden_edge_attrs():
return ['color="#eef2f7"', 'fontcolor="#eef2f7"']
Comment thread
indigo1973 marked this conversation as resolved.

for pred, succ in edges:
hidden = (pred, succ) in hidden_edges
hidden_attrs = hidden_edge_attrs() if hidden else []
if not show_tensor:
lines.append(f" {_node_id(pred)} -> {_node_id(succ)};")
lines.append(f" {_node_id(pred)} -> {_node_id(succ)}{edge_attr_str(hidden_attrs)};")
continue
rows = annotations.get((pred, succ), [])
if not rows:
lines.append(f" {_node_id(pred)} -> {_node_id(succ)};")
lines.append(f" {_node_id(pred)} -> {_node_id(succ)}{edge_attr_str(hidden_attrs)};")
continue
for row in rows:
arg = row.get("arg")
Expand All @@ -782,19 +795,21 @@ def emit_dot(
tail = ""
head = ""
edge_attrs = []
if hidden:
edge_attrs.extend(hidden_edge_attrs())
arg_idx = _normalize_small_int(arg)
if arg_idx is not None and arg_idx >= 0:
head = f":in_{arg_idx}:w"
out_port = _producer_output_port(task_table.get(pred), tid)
if out_port:
tail = f":{out_port}:e"
if source == "explicit":
if source == "explicit" and not hidden:
edge_attrs.append('style="dashed"')
edge_attrs.append('color="#B0B0B0"')
overlap = row.get("overlap")
if overlap and overlap != "covered":
if overlap and overlap != "covered" and not hidden:
edge_attrs.append(f'label="{_html_escape(overlap)}", fontsize=8, fontcolor="#C04040"')
attr_str = (" [" + ", ".join(edge_attrs) + "]") if edge_attrs else ""
attr_str = edge_attr_str(edge_attrs)
lines.append(f" {_node_id(pred)}{tail} -> {_node_id(succ)}{head}{attr_str};")
lines.append("}")
return "\n".join(lines) + "\n"
Expand Down Expand Up @@ -1027,8 +1042,14 @@ def emit_html(
tensor_table=None,
task_table=None,
show_tensor_info=None,
html_edge_style=None,
):
"""Build the pan/zoom HTML page: DOT → Graphviz SVG → inline into template."""
html_edge_style = html_edge_style or {}
hidden_edges = html_edge_style.get("hidden_edges")
visible_edge_count = html_edge_style.get("visible_edge_count")
if visible_edge_count is None:
visible_edge_count = len(edges)
dot = emit_dot(
edges,
nodes,
Expand All @@ -1038,14 +1059,15 @@ def emit_html(
tensor_table=tensor_table,
task_table=task_table,
show_tensor_info=show_tensor_info,
hidden_edges=hidden_edges,
)
svg_bytes = render_svg(dot, engine=engine)
svg_text = svg_bytes.decode("utf-8", errors="replace")
if "<svg" in svg_text:
svg_text = svg_text[svg_text.index("<svg") :]
return _HTML_TEMPLATE.format(
n_nodes=len(nodes),
n_edges=len(edges),
n_edges=visible_edge_count,
svg_body=svg_text,
spmd_badges_json=_spmd_badges_json(nodes, task_table),
)
Expand Down Expand Up @@ -1094,8 +1116,8 @@ def _build_parser():
%(prog)s deps.json --format text -o graph.txt
%(prog)s deps.json --format html --engine sfdp
%(prog)s deps.json --format html --show-tensor-info
%(prog)s deps.json --edge-mode reduced # drop transitively-implied edges, print what was removed
%(prog)s deps.json --edge-mode omitted # draw ONLY the transitively-implied (redundant) edges
%(prog)s deps.json --edge-mode reduced # select non-redundant edges, print what was removed
%(prog)s deps.json --edge-mode omitted # select transitively-implied (redundant) edges
""",
)
p.add_argument("input", nargs="?", help="Path to deps.json (default: newest under ./outputs/).")
Expand All @@ -1111,10 +1133,11 @@ def _build_parser():
choices=["full", "reduced", "omitted"],
default="full",
help=(
"full (default) draws every dependency edge; reduced applies transitive reduction, keeping the "
"minimal edge set; omitted keeps only the redundant edges reduced would drop (the complement of "
"full (default) selects every dependency edge; reduced applies transitive reduction, selecting the "
"minimal edge set; omitted selects only the redundant edges reduced would drop (the complement of "
"reduced). reduced/omitted print the redundant edges to stdout, are structural (pred,succ) level, "
"apply to both text and html, and are skipped with a warning on a cyclic graph."
"apply to both text and html, and are skipped with a warning on a cyclic graph. In html, all edges "
"still participate in layout; unselected edges are colored as background."
Comment thread
indigo1973 marked this conversation as resolved.
),
)
p.add_argument(
Expand Down Expand Up @@ -1184,6 +1207,8 @@ def main(argv=None):
nodes = sorted(set(nodes) | set(meta.keys()), key=_sort_task_id_key)

mode_stem = "deps_viewer"
hidden_html_edges = set()
visible_html_edge_count = len(edges)
if args.edge_mode in ("reduced", "omitted"):
kept, removed, is_dag = _transitive_reduction(edges, nodes)
if not is_dag:
Expand All @@ -1205,12 +1230,20 @@ def main(argv=None):
print(f"Redundant edges only: showing {len(removed)} redundant edge(s) of {len(edges)}")
for u, v in removed:
print(f" - {fmt_task(u)} -> {fmt_task(v)}")
edges = shown
# Keep only the annotations of the shown edges so annotated-edge
# counts (emit_text summary, the final print) stay consistent with
# the rendered edge set instead of counting the hidden ones.
shown_set = set(shown)
annotations = {k: v for k, v in annotations.items() if k in shown_set}
if args.format == "html":
hidden_html_edges = set(removed if args.edge_mode == "reduced" else kept)
visible_html_edge_count = len(shown)
print(
f"HTML layout preserves all {len(edges)} edge(s); "
f"{len(hidden_html_edges)} unselected edge(s) are colored as background"
)
Comment thread
indigo1973 marked this conversation as resolved.
else:
edges = shown
# Keep only the annotations of the shown edges so annotated-edge
# counts (emit_text summary, the final print) stay consistent with
# the rendered edge set instead of counting the hidden ones.
shown_set = set(shown)
annotations = {k: v for k, v in annotations.items() if k in shown_set}
mode_stem = f"deps_viewer_{args.edge_mode}"

out = (
Expand Down Expand Up @@ -1247,9 +1280,19 @@ def main(argv=None):
tensor_table=tensor_table,
task_table=task_table,
show_tensor_info=args.show_tensor_info,
html_edge_style={
"hidden_edges": hidden_html_edges,
"visible_edge_count": visible_html_edge_count,
},
)
out.write_text(html)
print(f"Wrote {out} ({len(nodes)} nodes, {len(edges)} edges, engine={args.engine}, format=html)")
if hidden_html_edges:
print(
f"Wrote {out} ({len(nodes)} nodes, {visible_html_edge_count} visible edges, "
f"{len(edges)} layout edges, engine={args.engine}, format=html)"
)
else:
print(f"Wrote {out} ({len(nodes)} nodes, {len(edges)} edges, engine={args.engine}, format=html)")
return 0


Expand Down
96 changes: 66 additions & 30 deletions tests/ut/py/test_deps_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,19 @@ def test_emit_dot_handles_missing_task_table():
assert 'label="1 · alloc"' in dot


def test_emit_dot_hides_selected_edges_with_background_color():
dot = deps_viewer.emit_dot(
edges=[(1, 2), (2, 3)],
nodes=[1, 2, 3],
meta={},
task_table=None,
hidden_edges={(1, 2)},
)

assert 'T0_1 -> T0_2 [color="#eef2f7", fontcolor="#eef2f7"];' in dot
Comment thread
indigo1973 marked this conversation as resolved.
assert "T0_2 -> T0_3;" in dot


def test_emit_html_default_preserves_task_table_rendering(monkeypatch):
captured = {}

Expand Down Expand Up @@ -285,17 +298,9 @@ def test_main_passes_task_table_when_show_tensor_info_enabled(tmp_path, monkeypa

captured = {}

def fake_emit_html(
edges,
nodes,
meta,
direction="LR",
engine="dot",
annotations=None,
tensor_table=None,
task_table=None,
show_tensor_info=False,
):
def fake_emit_html(*args, **kwargs):
task_table = kwargs["task_table"]
show_tensor_info = kwargs["show_tensor_info"]
captured["task_table"] = task_table
captured["show_tensor_info"] = show_tensor_info
return "<html></html>"
Expand Down Expand Up @@ -329,17 +334,9 @@ def test_main_keeps_task_metadata_when_show_tensor_info_disabled(tmp_path, monke

captured = {}

def fake_emit_html(
edges,
nodes,
meta,
direction="LR",
engine="dot",
annotations=None,
tensor_table=None,
task_table=None,
show_tensor_info=False,
):
def fake_emit_html(*args, **kwargs):
task_table = kwargs["task_table"]
show_tensor_info = kwargs["show_tensor_info"]
captured["task_table"] = task_table
captured["show_tensor_info"] = show_tensor_info
return "<html></html>"
Expand All @@ -353,14 +350,6 @@ def fake_emit_html(
assert captured["show_tensor_info"] is False


def test_transitive_reduction_removes_implied_edge():
# A->B->C plus the implied shortcut A->C.
kept, removed, is_dag = deps_viewer._transitive_reduction([(1, 2), (1, 3), (2, 3)], [1, 2, 3])
assert is_dag
assert removed == [(1, 3)]
assert kept == [(1, 2), (2, 3)]


def test_transitive_reduction_keeps_non_redundant_diamond():
# Diamond A->B, A->C, B->D, C->D — no edge is implied by another path.
edges = [(1, 2), (1, 3), (2, 4), (3, 4)]
Expand Down Expand Up @@ -430,6 +419,53 @@ def test_main_full_mode_keeps_all_edges(tmp_path, capsys):
assert "Transitive reduction" not in capsys.readouterr().out


def test_main_html_edge_modes_keep_full_layout_and_hide_unselected_edges(tmp_path, monkeypatch):
ring = 1 << 32
a, b, c = ring + 1, ring + 2, ring + 3
all_edges = [(a, b), (b, c), (a, c)]
deps_path = _write_deps_edges(tmp_path, all_edges)
captures = []

def fake_emit_html(edges, _nodes, _meta, **kwargs):
html_edge_style = kwargs["html_edge_style"]
captures.append(
{
"edges": edges,
"annotations": kwargs["annotations"],
"hidden_edges": html_edge_style["hidden_edges"],
"visible_edge_count": html_edge_style["visible_edge_count"],
}
)
return "<html></html>"

monkeypatch.setattr(deps_viewer, "emit_html", fake_emit_html)

cases = [
("reduced", {(a, c)}, 2),
("omitted", {(a, b), (b, c)}, 1),
]
for mode, expected_hidden, expected_visible_count in cases:
rc = deps_viewer.main(
[
str(deps_path),
"--format",
"html",
"--edge-mode",
mode,
"-o",
str(tmp_path / f"{mode}.html"),
]
)
assert rc == 0

assert len(captures) == len(cases)
for captured, (_mode, expected_hidden, expected_visible_count) in zip(captures, cases):
assert captured["edges"] == sorted(all_edges)
assert set(captured["annotations"]) == set(all_edges)
assert captured["hidden_edges"] == expected_hidden
assert captured["visible_edge_count"] == expected_visible_count


def test_main_omitted_mode_draws_only_redundant_edges(tmp_path, capsys):
# A(r1t1) -> B(r1t2) -> C(r1t3) plus the implied A->C shortcut. omitted
# keeps ONLY the redundant A->C edge (complement of reduced).
Expand Down
Loading