Download scripts/clean_eval_utils.py from kliu14/icflownet: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/kliu14/icflownet/resolve/main/scripts/clean_eval_utils.py
- Command line
-
hf download hf://kliu14/icflownet/scripts/clean_eval_utils.py
-
curl -L -o clean_eval_utils.py https://huggingface.co/kliu14/icflownet/resolve/main/scripts/clean_eval_utils.py
16.1 kB
| """Shared utilities for distributed clean-test evaluation.""" | |
| import gzip | |
| import hashlib | |
| import json | |
| import os | |
| import random | |
| import shutil | |
| import tempfile | |
| import time | |
| from collections import Counter, defaultdict | |
| from datetime import timedelta | |
| import dgl | |
| import torch | |
| import torch.distributed as dist | |
| import torch.nn.functional as F | |
| from common import EDGE_TYPE_MAPPING, build_paper_mtl_augmented_graph | |
| TASKS = ("ret", "jumptable", "indirectcall", "tailcall") | |
| def distributed_setup(): | |
| rank = int(os.environ.get("RANK", "0")) | |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) | |
| world_size = int(os.environ.get("WORLD_SIZE", "1")) | |
| if world_size > 1: | |
| backend = os.environ.get("ICFLOWNET_DIST_BACKEND", "nccl") | |
| dist.init_process_group(backend=backend, timeout=timedelta(hours=12)) | |
| torch.cuda.set_device(local_rank) | |
| return rank, local_rank, world_size, torch.device("cuda", local_rank) | |
| def cleanup_distributed(world_size): | |
| if world_size > 1 and dist.is_initialized(): | |
| dist.destroy_process_group() | |
| def all_reduce(tensor, world_size): | |
| if world_size > 1: | |
| if dist.get_backend() == "gloo" and tensor.is_cuda: | |
| cpu_tensor = tensor.cpu() | |
| dist.all_reduce(cpu_tensor, op=dist.ReduceOp.SUM) | |
| tensor.copy_(cpu_tensor) | |
| else: | |
| dist.all_reduce(tensor, op=dist.ReduceOp.SUM) | |
| return tensor | |
| def seed_everything(seed, rank): | |
| random.seed(seed + rank) | |
| torch.manual_seed(seed + rank) | |
| torch.cuda.manual_seed_all(seed + rank) | |
| def load_json(path): | |
| with open(path, "r") as handle: | |
| return json.load(handle) | |
| def read_jsonl(path): | |
| rows = [] | |
| with open(path, "r") as handle: | |
| for line in handle: | |
| line = line.strip() | |
| if line: | |
| rows.append(json.loads(line)) | |
| return rows | |
| def load_compressed_graph(path, use_func_rel=False): | |
| cache_dir = os.environ.get("ICFLOWNET_CLEAN_GRAPH_CACHE_DIR") | |
| cached_path = None | |
| if cache_dir: | |
| os.makedirs(cache_dir, exist_ok=True) | |
| path_digest = hashlib.sha256(os.path.abspath(path).encode("utf-8")).hexdigest()[:16] | |
| graph_name = os.path.basename(path) | |
| if graph_name.endswith(".gz"): | |
| graph_name = graph_name[:-3] | |
| cached_path = os.path.join(cache_dir, path_digest + "_" + graph_name) | |
| if os.path.exists(cached_path): | |
| graph = dgl.load_graphs(cached_path)[0][0] | |
| if not use_func_rel and "code2funchead" in graph.etypes: | |
| edge_ids = graph.edges(form="eid", etype="code2funchead") | |
| graph = dgl.remove_edges(graph, edge_ids, etype="code2funchead") | |
| return graph | |
| with tempfile.NamedTemporaryFile( | |
| prefix="icflownet_eval_", | |
| suffix=".graph", | |
| dir=cache_dir, | |
| delete=False, | |
| ) as temporary: | |
| temporary_path = temporary.name | |
| try: | |
| with gzip.open(path, "rb") as source, open(temporary_path, "wb") as destination: | |
| shutil.copyfileobj(source, destination) | |
| if cached_path: | |
| os.replace(temporary_path, cached_path) | |
| temporary_path = cached_path | |
| graph = dgl.load_graphs(temporary_path)[0][0] | |
| if not use_func_rel and "code2funchead" in graph.etypes: | |
| edge_ids = graph.edges(form="eid", etype="code2funchead") | |
| graph = dgl.remove_edges(graph, edge_ids, etype="code2funchead") | |
| return graph | |
| finally: | |
| if not cached_path and os.path.exists(temporary_path): | |
| os.remove(temporary_path) | |
| def discard_unused_full_code_features(graph): | |
| """Keep pooled code features when both pooled and full tensors are stored.""" | |
| if "code" in graph.ntypes: | |
| code_data = graph.nodes["code"].data | |
| if "featmean" in code_data and "feat" in code_data: | |
| del code_data["feat"] | |
| return graph | |
| def prepare_clean_records(clean_root, resource_root, scope="overall"): | |
| if scope not in ("overall", "long_range"): | |
| raise ValueError("Unsupported clean-test scope: {}".format(scope)) | |
| records = read_jsonl(os.path.join(clean_root, "binary_graph_mapping.jsonl")) | |
| prepared = [] | |
| pair_totals = Counter() | |
| task_totals = Counter() | |
| zero_pair_records = [] | |
| for record in records: | |
| binary_key = record["binary_key"] | |
| resource_dir = os.path.join(resource_root, binary_key) | |
| lookup_path = os.path.join(resource_dir, binary_key + "_nodelookup.json") | |
| nidtoaddr_path = os.path.join(resource_dir, binary_key + "_nidtoaddr.json") | |
| hubmeta_path = os.path.join(resource_dir, binary_key + "_hubmeta.json") | |
| graphstats_path = os.path.join(resource_dir, binary_key + "_graphstats.json") | |
| graph_path = os.path.join(clean_root, record["graph"]) | |
| required = (lookup_path, hubmeta_path, graphstats_path, graph_path) | |
| missing = [path for path in required if not os.path.exists(path)] | |
| if missing: | |
| raise RuntimeError( | |
| "Missing clean-test resources for {}: {}".format(binary_key, missing) | |
| ) | |
| graphstats = load_json(graphstats_path) | |
| expected_code_nodes = int(graphstats["code_nodes"]) | |
| # New clean-test resources explicitly preserve separate code/data node-id | |
| # namespaces in ``nidtoaddr``. Their legacy nodelookup can therefore | |
| # contain overlapping local IDs from both node types and must not be | |
| # interpreted as a code-only lookup. Prefer the unambiguous code map | |
| # when it is available, while retaining compatibility with older data. | |
| if os.path.exists(nidtoaddr_path): | |
| nidtoaddr = load_json(nidtoaddr_path) | |
| code_nidtoaddr = nidtoaddr.get("code") | |
| else: | |
| code_nidtoaddr = None | |
| if isinstance(code_nidtoaddr, dict): | |
| lookup = { | |
| str(address): int(node_id) | |
| for node_id, address in code_nidtoaddr.items() | |
| } | |
| else: | |
| raw_lookup = load_json(lookup_path) | |
| lookup = { | |
| str(address): int(node_id) | |
| for address, node_id in raw_lookup.items() | |
| } | |
| if set(lookup.values()) != set(range(expected_code_nodes)): | |
| raise RuntimeError( | |
| "Node lookup does not exactly cover graph code ids for {}.".format( | |
| binary_key | |
| ) | |
| ) | |
| task_rows = defaultdict(list) | |
| record_pair_count = 0 | |
| for kind in (scope + "_positive", scope + "_negative"): | |
| rows = read_jsonl(os.path.join(clean_root, record[kind])) | |
| pair_totals[kind] += len(rows) | |
| record_pair_count += len(rows) | |
| for row in rows: | |
| src_key = str(row["src"]) | |
| dst_key = str(row["dst"]) | |
| if src_key not in lookup or dst_key not in lookup: | |
| raise RuntimeError( | |
| "Unmapped clean pair in {}: {}".format(binary_key, row) | |
| ) | |
| task = row["task"] | |
| task_rows[task].append( | |
| { | |
| "src": lookup[src_key], | |
| "dst": lookup[dst_key], | |
| "label": int(row["label"]), | |
| } | |
| ) | |
| task_totals[(task, int(row["label"]))] += 1 | |
| if record_pair_count == 0: | |
| zero_pair_records.append( | |
| {"graph_id": str(record["graph_id"]), "binary_key": binary_key} | |
| ) | |
| prepared.append( | |
| { | |
| "binary_key": binary_key, | |
| "graph_id": str(record["graph_id"]), | |
| "graph_path": graph_path, | |
| "candidate_meta": load_json(hubmeta_path), | |
| "task_rows": dict(task_rows), | |
| "expected_code_nodes": expected_code_nodes, | |
| "expected_data_nodes": int(graphstats["data_nodes"]), | |
| } | |
| ) | |
| return prepared, { | |
| "records": len(prepared), | |
| "pair_totals": dict(pair_totals), | |
| "nonzero_pair_records": len(prepared) - len(zero_pair_records), | |
| "zero_pair_records": zero_pair_records, | |
| "task_label_totals": { | |
| "{}:{}".format(task, label): count | |
| for (task, label), count in sorted(task_totals.items()) | |
| }, | |
| } | |
| def confusion_metrics(tn, fp, fn, tp): | |
| precision = tp / (tp + fp) if tp + fp else 0.0 | |
| recall = tp / (tp + fn) if tp + fn else 0.0 | |
| f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0 | |
| return { | |
| "precision": precision, | |
| "recall": recall, | |
| "f1": f1, | |
| "support": tp + fn, | |
| "pairs": tn + fp + fn + tp, | |
| "tn": tn, | |
| "fp": fp, | |
| "fn": fn, | |
| "tp": tp, | |
| "confusion_matrix": [[tn, fp], [fn, tp]], | |
| } | |
| def evaluate_clean_full( | |
| ddp_model, | |
| clean_records, | |
| device, | |
| rank, | |
| world_size, | |
| output_dir, | |
| log_every, | |
| amp_enabled, | |
| ): | |
| module = ddp_model.module | |
| module.eval() | |
| local_records = clean_records[rank::world_size] | |
| confusions = torch.zeros((len(TASKS), 2, 2), dtype=torch.long, device=device) | |
| loss_sums = torch.zeros(len(TASKS), dtype=torch.float64, device=device) | |
| pair_counts = torch.zeros(len(TASKS), dtype=torch.float64, device=device) | |
| type_confusion = torch.zeros((4, 4), dtype=torch.long, device=device) | |
| evaluated = torch.zeros((), dtype=torch.long, device=device) | |
| local_details = [] | |
| start_time = time.time() | |
| for item_index, item in enumerate(local_records, start=1): | |
| combined_pairs = [] | |
| task_slices = {} | |
| actual_types = [] | |
| actual_labels = [] | |
| for task in TASKS: | |
| rows = item["task_rows"].get(task, []) | |
| if not rows: | |
| continue | |
| start = len(combined_pairs) | |
| combined_pairs.extend((row["src"], row["dst"]) for row in rows) | |
| actual_labels.extend(row["label"] for row in rows) | |
| actual_types.extend([EDGE_TYPE_MAPPING[task]] * len(rows)) | |
| task_slices[task] = (start, len(combined_pairs), rows) | |
| if not combined_pairs: | |
| local_details.append( | |
| { | |
| "binary": item["binary_key"], | |
| "graph_id": item["graph_id"], | |
| "pairs": 0, | |
| "tasks": {}, | |
| "metric_contribution": "none_no_labeled_pairs", | |
| } | |
| ) | |
| evaluated += 1 | |
| _log_progress(rank, item_index, len(local_records), log_every, start_time) | |
| continue | |
| base_graph = discard_unused_full_code_features( | |
| load_compressed_graph(item["graph_path"]) | |
| ) | |
| if ( | |
| base_graph.num_nodes("code") != item["expected_code_nodes"] | |
| or base_graph.num_nodes("data") != item["expected_data_nodes"] | |
| ): | |
| raise RuntimeError( | |
| "Clean graph node count mismatch for {}.".format(item["binary_key"]) | |
| ) | |
| model_graph = build_paper_mtl_augmented_graph( | |
| base_graph, | |
| item["candidate_meta"], | |
| use_gch=True, | |
| use_gdh=True, | |
| task_aware_routing=True, | |
| gdh_radius=2, | |
| ) | |
| graph_gpu = model_graph.to(device) | |
| pairs = torch.tensor(combined_pairs, dtype=torch.long, device=device) | |
| labels = torch.tensor(actual_labels, dtype=torch.float32, device=device) | |
| types = torch.tensor(actual_types, dtype=torch.long, device=device) | |
| with torch.no_grad(), torch.autocast( | |
| device_type=device.type, dtype=torch.float16, enabled=amp_enabled | |
| ): | |
| task_logits, edge_type_logits = module( | |
| graph_gpu, pairs, candidate_meta=None | |
| ) | |
| detail = { | |
| "binary": item["binary_key"], | |
| "graph_id": item["graph_id"], | |
| "pairs": len(combined_pairs), | |
| "tasks": {}, | |
| } | |
| for task_index, task in enumerate(TASKS): | |
| if task not in task_slices: | |
| continue | |
| start, end, rows = task_slices[task] | |
| logits = task_logits[task].reshape(-1)[start:end] | |
| task_labels = labels[start:end] | |
| scores = torch.sigmoid(logits) | |
| predictions = scores >= 0.5 | |
| positives = task_labels == 1 | |
| negatives = ~positives | |
| tn = int(((~predictions) & negatives).sum().item()) | |
| fp = int((predictions & negatives).sum().item()) | |
| fn = int(((~predictions) & positives).sum().item()) | |
| tp = int((predictions & positives).sum().item()) | |
| confusions[task_index, 0, 0] += tn | |
| confusions[task_index, 0, 1] += fp | |
| confusions[task_index, 1, 0] += fn | |
| confusions[task_index, 1, 1] += tp | |
| loss_sums[task_index] += F.binary_cross_entropy_with_logits( | |
| logits, task_labels, reduction="sum" | |
| ).double() | |
| pair_counts[task_index] += len(rows) | |
| task_detail = { | |
| "pairs": len(rows), | |
| "confusion_matrix": [[tn, fp], [fn, tp]], | |
| } | |
| detail["tasks"][task] = task_detail | |
| positive_mask = labels == 1 | |
| predicted_types = edge_type_logits[positive_mask].argmax(dim=1) | |
| true_types = types[positive_mask] | |
| for true_type, predicted_type in zip( | |
| true_types.tolist(), predicted_types.tolist() | |
| ): | |
| type_confusion[true_type, predicted_type] += 1 | |
| local_details.append(detail) | |
| evaluated += 1 | |
| del graph_gpu, base_graph, model_graph, pairs, labels, types | |
| del task_logits, edge_type_logits | |
| if device.type == "cuda": | |
| torch.cuda.empty_cache() | |
| _log_progress(rank, item_index, len(local_records), log_every, start_time) | |
| detail_dir = os.path.join(output_dir, "clean_details") | |
| os.makedirs(detail_dir, exist_ok=True) | |
| detail_path = os.path.join(detail_dir, "rank_{}.jsonl".format(rank)) | |
| with open(detail_path, "w") as handle: | |
| for detail in local_details: | |
| handle.write(json.dumps(detail) + "\n") | |
| for tensor in (confusions, loss_sums, pair_counts, type_confusion, evaluated): | |
| all_reduce(tensor, world_size) | |
| confusions = confusions.cpu() | |
| loss_sums = loss_sums.cpu() | |
| pair_counts = pair_counts.cpu() | |
| task_metrics = {} | |
| for task_index, task in enumerate(TASKS): | |
| tn = int(confusions[task_index, 0, 0].item()) | |
| fp = int(confusions[task_index, 0, 1].item()) | |
| fn = int(confusions[task_index, 1, 0].item()) | |
| tp = int(confusions[task_index, 1, 1].item()) | |
| task_metrics[task] = confusion_metrics(tn, fp, fn, tp) | |
| task_metrics[task]["loss"] = float( | |
| loss_sums[task_index].item() / max(pair_counts[task_index].item(), 1) | |
| ) | |
| type_confusion = type_confusion.cpu() | |
| type_total = int(type_confusion.sum().item()) | |
| type_correct = int(type_confusion.diag().sum().item()) | |
| return { | |
| "evaluated_binaries": int(evaluated.item()), | |
| "task_metrics": task_metrics, | |
| "macro_f1": sum(value["f1"] for value in task_metrics.values()) / len(TASKS), | |
| "edge_type": { | |
| "class_order": ["ret", "jumptable", "tailcall", "indirectcall"], | |
| "confusion_matrix": type_confusion.tolist(), | |
| "accuracy": type_correct / type_total if type_total else 0.0, | |
| "positive_pairs": type_total, | |
| }, | |
| "detail_rank_files": [ | |
| os.path.join(detail_dir, "rank_{}.jsonl".format(worker_rank)) | |
| for worker_rank in range(world_size) | |
| ], | |
| "elapsed_seconds": time.time() - start_time, | |
| } | |
| def _log_progress(rank, current, total, log_every, start_time): | |
| if current == 1 or current % log_every == 0 or current == total: | |
| print( | |
| "[clean][rank {}] graphs={}/{} elapsed={:.1f}s".format( | |
| rank, current, total, time.time() - start_time | |
| ), | |
| flush=True, | |
| ) | |