-
Notifications
You must be signed in to change notification settings - Fork 49
feat(tools): add backward graph generation and validation tools #711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 5 commits
67a81d6
ea832a1
210a51f
af8fd20
db5f971
81a157a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,14 +27,18 @@ def __call__(self): | |
| module, forward_inputs = get_torch_module_and_inputs( | ||
| self.model_path, use_dummy_inputs=False, device=self.device | ||
| ) | ||
| module.train() | ||
| module.eval() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. model.eval() 不会禁用梯度计算,只有 torch.no_grad() / torch.inference_mode() 才会。eval 仅改变特定层的前向行为(dropout → identity,BatchNorm → 用 running stats 而非 batch stats),反向传播完全正常。而且使用 eval 模式反而更好
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 不行,反向图生成时,这些算子就应该用
Dayuxiaoshui marked this conversation as resolved.
|
||
|
|
||
| eval_forward_dir = os.path.join( | ||
| self.output_dir, "eval_forward", self.rel_model_path | ||
| ) | ||
| if not os.path.exists(eval_forward_dir): | ||
| shutil.copytree(self.model_path, eval_forward_dir) | ||
|
|
||
| forward_inputs = [ | ||
| inp.detach().clone() if isinstance(inp, torch.Tensor) else inp | ||
| for inp in forward_inputs | ||
| ] | ||
| forward_inputs = self.set_requires_grad_for_forward_inputs( | ||
| self.model_path, module, forward_inputs | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| """Deduplicate Triton kernels by source content across extracted samples. | ||
|
|
||
| Reads the output of the ``extract`` pipeline step (``triton_kernel/*.py`` | ||
| files under each sample directory) and identifies duplicate kernels — kernels | ||
| with identical source code that appear in multiple samples. | ||
|
|
||
| This is distinct from ``graph_hash.txt``-based dedup which identifies | ||
| identical *subgraphs*. Kernel-level dedup identifies identical *compiled | ||
| kernels* produced by TorchInductor, which may come from different subgraphs | ||
| that share the same operator patterns (e.g., two different backward graphs | ||
| both containing ``layer_norm_bwd``). | ||
|
|
||
| Usage (as subcommand):: | ||
|
|
||
| python3 -m tools.triton_kernel_extractor dedup \\ | ||
| --input-dir /data/output/extracted \\ | ||
| --output /tmp/dedup_report.json | ||
|
|
||
| Usage (standalone):: | ||
|
|
||
| python3 tools/triton_kernel_extractor/kernel_dedup.py \\ | ||
| --input-dir /data/output/extracted \\ | ||
| --output /tmp/dedup_report.json | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import hashlib | ||
| import json | ||
| import logging | ||
| import os | ||
| from collections import Counter | ||
| from pathlib import Path | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _normalize_kernel_source(source: str) -> str: | ||
| """Strip comments and blank lines to produce a normalized form for hashing.""" | ||
| lines: list[str] = [] | ||
| for line in source.splitlines(): | ||
| stripped = line.strip() | ||
| if not stripped: | ||
| continue | ||
| if stripped.startswith("#"): | ||
| continue | ||
| lines.append(stripped) | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def _hash_source(normalized: str) -> str: | ||
| """Compute a stable MD5 hash of the normalized kernel source.""" | ||
| return hashlib.md5(normalized.encode("utf-8")).hexdigest() | ||
|
|
||
|
|
||
| def dedup_kernels(input_dir: Path) -> dict: | ||
| """Walk *input_dir* for ``triton_kernel/*.py`` files and compute dedup stats. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| input_dir: | ||
| Root directory containing per-sample subdirectories. Each sample | ||
| directory is expected to have a ``triton_kernel/`` subdirectory | ||
| with ``.py`` files produced by the ``extract`` pipeline step. | ||
|
|
||
| Returns | ||
| ------- | ||
| dict with keys: | ||
| - ``total_samples``: number of sample directories scanned | ||
| - ``total_kernel_instances``: total ``.py`` files found | ||
| - ``unique_kernel_hashes``: number of distinct (normalized) source hashes | ||
| - ``dedup_rate_percent``: ``(1 - unique / total) * 100`` | ||
| - ``avg_kernels_per_sample``: mean kernel count per sample | ||
| - ``kernel_name_freq``: ``{kernel_name: occurrence_count}`` across all samples | ||
| - ``per_sample``: list of ``{path, kernel_count, hashes}`` per sample | ||
| """ | ||
| if not input_dir.is_dir(): | ||
| logger.error("Input directory does not exist: %s", input_dir) | ||
| return {} | ||
|
|
||
| # Enumerate sample directories (any dir containing triton_kernel/). | ||
| samples: list[Path] = [] | ||
| for dirpath, dirnames, _ in os.walk(str(input_dir)): | ||
| if "triton_kernel" in dirnames: | ||
| samples.append(Path(dirpath)) | ||
|
|
||
| if not samples: | ||
| logger.warning("No samples with triton_kernel/ found under %s", input_dir) | ||
| return {} | ||
|
|
||
| all_hashes: list[str] = [] | ||
| kernel_name_counter: Counter[str] = Counter() | ||
| per_sample: list[dict] = [] | ||
|
|
||
| for sample_dir in sorted(samples): | ||
| kernel_dir = sample_dir / "triton_kernel" | ||
| kernel_files = sorted(kernel_dir.glob("*.py")) | ||
| hashes: list[str] = [] | ||
| for kf in kernel_files: | ||
| try: | ||
| source = kf.read_text(encoding="utf-8", errors="replace") | ||
| except OSError: | ||
| logger.warning("Cannot read kernel file: %s", kf) | ||
| continue | ||
| normalized = _normalize_kernel_source(source) | ||
| h = _hash_source(normalized) | ||
| hashes.append(h) | ||
| all_hashes.append(h) | ||
| kernel_name_counter[kf.stem] += 1 | ||
|
|
||
| per_sample.append( | ||
| { | ||
| "sample": str(sample_dir), | ||
| "kernel_count": len(hashes), | ||
| "hashes": hashes, | ||
| } | ||
| ) | ||
|
|
||
| total = len(all_hashes) | ||
| unique = len(set(all_hashes)) | ||
| dedup_rate = round((1 - unique / total) * 100, 2) if total > 0 else 0.0 | ||
| avg = round(total / len(samples), 2) if samples else 0.0 | ||
|
|
||
| return { | ||
| "total_samples": len(samples), | ||
| "total_kernel_instances": total, | ||
| "unique_kernel_hashes": unique, | ||
| "dedup_rate_percent": dedup_rate, | ||
| "avg_kernels_per_sample": avg, | ||
| "kernel_name_freq": dict(kernel_name_counter.most_common()), | ||
| "per_sample": per_sample, | ||
| } | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # CLI | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> None: | ||
| parser = argparse.ArgumentParser( | ||
| description="Deduplicate Triton kernels by source content.", | ||
| ) | ||
| parser.add_argument( | ||
| "--input-dir", | ||
| type=Path, | ||
| required=True, | ||
| help=( | ||
| "Root directory containing per-sample subdirectories with " | ||
| "triton_kernel/*.py files (output of the extract pipeline step)." | ||
| ), | ||
| ) | ||
| parser.add_argument( | ||
| "--output", | ||
| type=Path, | ||
| required=True, | ||
| help="Output JSON path for the dedup report.", | ||
| ) | ||
| args = parser.parse_args(argv) | ||
|
|
||
| logging.basicConfig( | ||
| format="%(message)s", | ||
| level=logging.INFO, | ||
| ) | ||
|
|
||
| logger.info("Scanning: %s", args.input_dir) | ||
| report = dedup_kernels(args.input_dir) | ||
| if not report: | ||
| logger.warning("No data to report.") | ||
| return | ||
|
|
||
| # Print summary. | ||
| print("\n=== Kernel Dedup Report ===") | ||
| print(f"Total samples scanned: {report['total_samples']}") | ||
| print(f"Total kernel instances: {report['total_kernel_instances']}") | ||
| print(f"Unique kernel hashes: {report['unique_kernel_hashes']}") | ||
| print(f"Dedup rate: {report['dedup_rate_percent']}%") | ||
| print(f"Avg kernels per sample: {report['avg_kernels_per_sample']}") | ||
| print("\nTop kernel types by frequency:") | ||
| for name, count in list(report["kernel_name_freq"].items())[:20]: | ||
| bar = "#" * min(count, 40) | ||
| print(f" {name:50s} {count:4d} {bar}") | ||
|
|
||
| # Write JSON. | ||
| args.output.parent.mkdir(parents=True, exist_ok=True) | ||
| args.output.write_text( | ||
| json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8" | ||
| ) | ||
| print(f"\nReport saved to: {args.output}") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
调用
hash_util.py中的函数。