""" FusionOps Validator Validates agent actions before cost computation. Checks: op validity, dependency satisfaction, subgraph connectivity, memory limits. """ from __future__ import annotations from dataclasses import dataclass from typing import Optional from .models import Action, Graph, OpType, ScheduleState from .cost_model import classify_tensors, compute_tile_geometry, compute_working_set @dataclass class ValidationResult: is_valid: bool error: Optional[str] = None def validate_action( graph: Graph, action: Action, state: ScheduleState, ) -> ValidationResult: """ Validate an agent's action before execution. Returns ValidationResult with error message if invalid. """ op_ids = action.operation_ids config = action.config # 1. Check operation IDs are valid valid_op_ids = set(range(len(graph.operations))) for oid in op_ids: if oid not in valid_op_ids: return ValidationResult(False, f"Invalid operation ID: {oid}") # 2. Check operations are not already scheduled for oid in op_ids: if oid in state.scheduled_op_ids: return ValidationResult(False, f"Operation {oid} already scheduled") # 3. Check no duplicates in action if len(set(op_ids)) != len(op_ids): return ValidationResult(False, "Duplicate operation IDs in action") # 4. Check dependencies are satisfied # Every input tensor to the subgraph that comes from another op # must have its producer already scheduled (or be produced within this subgraph) op_id_set = set(op_ids) for oid in op_ids: op = graph.get_op(oid) for tid in op.input_tensor_ids: if tid in graph.tensor_producer: producer_id = graph.tensor_producer[tid] if producer_id not in state.scheduled_op_ids and producer_id not in op_id_set: return ValidationResult( False, f"Dependency not met: op {oid} needs tensor {tid} " f"from op {producer_id} which is not scheduled" ) # 5. Check subgraph connectivity # The ops must form a connected subgraph in the DAG if len(op_ids) > 1: if not _is_connected(graph, op_ids): return ValidationResult(False, "Operations do not form a connected subgraph") # 6. Check config validity if config.w <= 0 or config.h <= 0 or config.k <= 0: return ValidationResult(False, f"Config dimensions must be positive: [{config.w},{config.h},{config.k}]") # Check config divides tensor dimensions evenly or tiles correctly # (w and h just need to be <= tensor dims, tiling handles the rest) # But they should be powers of 2 or at least reasonable # For now, just check they're positive (the cost model handles tiling) # 7. Check tensors_to_retain validity # Can only retain tensors that are outputs of this subgraph produced_by_subgraph = set() for oid in op_ids: op = graph.get_op(oid) produced_by_subgraph.update(op.output_tensor_ids) for tid in action.tensors_to_retain: if tid not in produced_by_subgraph: return ValidationResult( False, f"Cannot retain tensor {tid}: not produced by this subgraph" ) # 8. Check working set fits in fast memory tensor_class = classify_tensors(graph, op_ids, state) ws = compute_working_set(graph, tensor_class, config, op_ids, state) if ws > graph.hardware.fast_memory_capacity: return ValidationResult( False, f"OOM: working set {ws:.0f} exceeds fast memory capacity " f"{graph.hardware.fast_memory_capacity}" ) # 9. Check traversal order validity (if provided) if action.traversal_order is not None: geom = compute_tile_geometry(graph, op_ids, config) expected_tiles = geom.total_spatial_tiles if len(action.traversal_order) != expected_tiles: return ValidationResult( False, f"Traversal order length {len(action.traversal_order)} " f"!= expected tiles {expected_tiles}" ) if set(action.traversal_order) != set(range(expected_tiles)): return ValidationResult( False, "Traversal order must be a permutation of tile indices" ) return ValidationResult(True) def _is_connected(graph: Graph, op_ids: list[int]) -> bool: """ Check if the given ops form a connected subgraph. Connected means: for any two ops in the set, there exists a path between them through tensor edges (ignoring direction). """ op_id_set = set(op_ids) if len(op_id_set) <= 1: return True # Build undirected adjacency within the subgraph adj: dict[int, set[int]] = {oid: set() for oid in op_ids} for oid in op_ids: op = graph.get_op(oid) # Check if any output tensor of this op is consumed by another op in the subgraph for tid in op.output_tensor_ids: if tid in graph.tensor_consumers: for consumer_id in graph.tensor_consumers[tid]: if consumer_id in op_id_set and consumer_id != oid: adj[oid].add(consumer_id) adj[consumer_id].add(oid) # Check if any input tensor of this op is produced by another op in the subgraph for tid in op.input_tensor_ids: if tid in graph.tensor_producer: producer_id = graph.tensor_producer[tid] if producer_id in op_id_set and producer_id != oid: adj[oid].add(producer_id) adj[producer_id].add(oid) # BFS from first op visited = set() queue = [op_ids[0]] visited.add(op_ids[0]) while queue: current = queue.pop(0) for neighbor in adj[current]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) return visited == op_id_set