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
72 changes: 8 additions & 64 deletions act/back_end/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,27 +60,8 @@ def get_bounds_for_var(self, fact: 'Fact', var_id: int, is_output: bool = True)

return lb, ub

def get_all_var_bounds(self, fact: 'Fact', is_output: bool = True) -> Dict[int, Tuple[float, float]]:
"""Get dict of {var_id: (lb, ub)} for all variables. Use is_output=True for out_vars, False for in_vars."""
var_list = self.out_vars if is_output else self.in_vars

bounds_dict = {}
for var_id in var_list:
bounds_dict[var_id] = self.get_bounds_for_var(fact, var_id, is_output)

return bounds_dict

def get_input_shape(self) -> Optional[Tuple[int, ...]]:
"""Get input shape from params, or None if not stored."""
if self.kind == "INPUT":
return self.params.get("shape")
return self.params.get("input_shape")

def get_output_shape(self) -> Optional[Tuple[int, ...]]:
"""Get output shape from params, or None if not stored."""
if self.kind == "INPUT":
return self.params.get("shape")
return self.params.get("output_shape")

def get_tensor(self, key: str, default: Optional[torch.Tensor] = None) -> Optional[torch.Tensor]:
"""Get tensor parameter. Returns default if missing or not a tensor."""
Expand All @@ -107,13 +88,7 @@ def is_tensor(self, key: str) -> bool:
"""Check if parameter is a tensor."""
return isinstance(self.params.get(key), torch.Tensor)

def get_num_input_vars(self) -> int:
"""Get number of input variables (flattened dimension)."""
return len(self.in_vars)

def get_num_output_vars(self) -> int:
"""Get number of output variables (flattened dimension)."""
return len(self.out_vars)

@dataclass
class Net:
Expand All @@ -132,10 +107,6 @@ def __post_init__(self):
validate_wrapper_graph(self.layers)

# helpers
def last_validation(self) -> Optional[Layer]:
for L in reversed(self.layers):
if L.is_validation(): return L
return None

def assert_last_is_validation(self) -> None:
if not self.layers or not self.layers[-1].is_validation():
Expand All @@ -158,12 +129,6 @@ def get_all_predecessor_bounds(self, layer_id: int, after: Dict[int, 'Fact'],
return [self.get_predecessor_bounds(layer_id, after, before, i)
for i in range(len(self.preds[layer_id]))]

def get_layer_shape(self, layer_id: int, facts: Dict[int, 'Fact'],
is_output: bool = True) -> Tuple[int, ...]:
"""Get shape from bounds tensor in facts dict. Use is_output=True for output shape, False for input."""
if layer_id not in facts:
raise KeyError(f"Layer {layer_id} not found in facts dictionary")
return facts[layer_id].bounds.lb.shape


def topological_sort(net: Net, reverse: bool = False) -> List[int]:
Expand Down Expand Up @@ -251,38 +216,17 @@ def add_box(self, layer_id: int, var_ids: List[int], B: Bounds):
"lb": B.lb.detach().reshape(-1).clone(),
"ub": B.ub.detach().reshape(-1).clone()}))

def add_op(self, tag: str, var_ids: List[int], **meta):
"""
Generic operator constraint container.
- tag: e.g. "dense:12", "relu:5"
- var_ids: ordered exactly as exporter expects
- meta: payload used by cons_exportor.py
"""
op = tag.split(":", 1)[0]
if op and not self._is_op_supported_by_exporter(op):
raise ValueError(
f"Unknown op tag '{op}' (tag='{tag}'). "
"Update act/back_end/layer_schema.py SUPPORTED_EXPORT_OPS "
"and exporter handling if intentional."
)
m = {"tag": tag}
m.update(meta)
self.replace(Con("INEQ", tuple(var_ids), m))

@staticmethod
def _is_op_supported_by_exporter(op: str) -> bool:
"""
Best-effort early validation against exporter registry.
Falls back to allow if exporter cannot be imported.
"""
try:
mod = importlib.import_module("act.back_end.layer_util")
fn = getattr(mod, "is_supported_op", None)
if fn is None:
return True
return bool(fn(op))
except Exception:
return True
"""Validate an operator against the exporter registry."""
mod = importlib.import_module("act.back_end.layer_util")
fn = getattr(mod, "is_supported_op", None)
if fn is None:
raise RuntimeError(
f"Exporter capability function is_supported_op is missing while checking {op!r}"
)
return bool(fn(op))

def __iter__(self):
"""Iterate over constraints (Con objects). Makes ConSet iterable."""
Expand Down
54 changes: 24 additions & 30 deletions act/back_end/hybridz_tf/hybridz_tf.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,20 +388,17 @@ def _sparse_fact(fact: Fact, hz: SparseHZono) -> Fact:

def _seed_sparse_cache(self, L: Layer, input_bounds: Bounds) -> None:
k = L.kind.upper()
try:
if k in ("INPUT", "INPUT_SPEC"):
if k in ("INPUT", "INPUT_SPEC"):
self._sparse_hz_cache[L.id] = self._sparse_from_bounds(input_bounds)
self._sparse_drop_reasons.pop(L.id, None)
elif k != "ASSERT":
preds = self._net.preds.get(L.id, [])
if preds and preds[0] in self._sparse_hz_cache:
self._sparse_hz_cache[L.id] = self._sparse_hz_cache[preds[0]]
self._sparse_drop_reasons.pop(L.id, None)
elif not preds:
self._sparse_hz_cache[L.id] = self._sparse_from_bounds(input_bounds)
self._sparse_drop_reasons.pop(L.id, None)
elif k != "ASSERT":
preds = self._net.preds.get(L.id, [])
if preds and preds[0] in self._sparse_hz_cache:
self._sparse_hz_cache[L.id] = self._sparse_hz_cache[preds[0]]
self._sparse_drop_reasons.pop(L.id, None)
elif not preds:
self._sparse_hz_cache[L.id] = self._sparse_from_bounds(input_bounds)
self._sparse_drop_reasons.pop(L.id, None)
except Exception as exc:
self._drop_sparse_hz(L.id, f"sparse_seed_failed:{type(exc).__name__}")

def _drop_sparse_hz(self, layer_id: int, reason: str) -> None:
lid = int(layer_id)
Expand All @@ -423,24 +420,21 @@ def _propagate_sparse_hz(self, L: Layer, input_bounds: Bounds, result: Fact) ->
if self._sparse_exceeds_limit(hz, result.bounds.lb.numel()):
self._drop_sparse_hz(L.id, f"sparse_size_limit:{k}")
return result
try:
for apply_sparse in (
hz_mlp.sparse_hz_apply_layer,
hz_cnn.sparse_hz_apply_layer,
hz_transformer.sparse_hz_apply_layer,
):
handled, out, drop_reason = apply_sparse(L, hz, input_bounds, result, self)
if not handled:
continue
if out is None:
self._drop_sparse_hz(L.id, drop_reason or f"unsupported_sparse_op:{k}")
return result
self._sparse_hz_cache[L.id] = out
self._sparse_drop_reasons.pop(L.id, None)
return self._sparse_fact(result, out)
self._drop_sparse_hz(L.id, f"unsupported_sparse_op:{k}")
except Exception as exc:
self._drop_sparse_hz(L.id, f"sparse_op_failed:{k}:{type(exc).__name__}")
for apply_sparse in (
hz_mlp.sparse_hz_apply_layer,
hz_cnn.sparse_hz_apply_layer,
hz_transformer.sparse_hz_apply_layer,
):
handled, out, drop_reason = apply_sparse(L, hz, input_bounds, result, self)
if not handled:
continue
if out is None:
self._drop_sparse_hz(L.id, drop_reason or f"unsupported_sparse_op:{k}")
return result
self._sparse_hz_cache[L.id] = out
self._sparse_drop_reasons.pop(L.id, None)
return self._sparse_fact(result, out)
self._drop_sparse_hz(L.id, f"unsupported_sparse_op:{k}")
return result

def apply(
Expand Down
18 changes: 13 additions & 5 deletions act/back_end/hybridz_tf/tf_mlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def _matmul_term_rows(L, batch: int):
y_shape = tuple(int(d) for d in L.params["y_shape"])
if len(x_shape) < 2 or len(y_shape) < 2 or x_shape[-1] != y_shape[-2]:
return None
rows = None
try:
batch_shape = np.broadcast_shapes(x_shape[:-2], y_shape[:-2])
m, reduction, n = x_shape[-2], x_shape[-1], y_shape[-1]
Expand All @@ -185,8 +186,13 @@ def _matmul_term_rows(L, batch: int):
np.swapaxes(y_local, -2, -1)[..., None, :, :],
(*batch_shape, m, n, reduction),
).reshape(-1, reduction)
rows = (x_terms, y_terms, reduction)
except (ValueError, TypeError):
# Non-broadcastable MATMUL metadata is not applicable to sparse lifting.
rows = None
if rows is None:
return None
x_terms, y_terms, reduction = rows
x_size, y_size = _prod(x_shape), _prod(y_shape)
offsets = np.arange(int(batch), dtype=np.int64)[:, None, None]
return (
Expand Down Expand Up @@ -1169,12 +1175,14 @@ def _row_indices_expand(L, n: int):
if per == 0 or int(n) % per != 0:
return None
batch = int(n) // per
try:
return torch.arange(int(n)).view(batch, *in_shape).broadcast_to(
batch, *out_shape
).reshape(-1)
except RuntimeError:
source_shape = (batch, *in_shape)
target_shape = (batch, *out_shape)
if len(source_shape) > len(target_shape) or any(
source not in (1, target)
for source, target in zip(reversed(source_shape), reversed(target_shape))
):
return None
return torch.arange(int(n)).view(source_shape).broadcast_to(target_shape).reshape(-1)


def _row_indices_reduce(L, n_in: int, n_out: int, axes_key: str, keepdims_key: str):
Expand Down
15 changes: 9 additions & 6 deletions act/back_end/hybridz_tf/tf_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,16 @@ def _attention_score_differences(L, bounds: Bounds, rowsize: int, tf):
continue
if kind in {"SCALE", "BIAS"}:
key = "a" if kind == "SCALE" else "c"
try:
value = (
_broadcast_flat(score_layer.params[key], n_scores)
.detach().cpu().double().numpy()
)
except ValueError:
raw_value = score_layer.params[key]
value_count = int(raw_value.numel())
if value_count == 0 or (
value_count not in (1, n_scores) and n_scores % value_count != 0
):
return None
value = (
_broadcast_flat(raw_value, n_scores)
.detach().cpu().double().numpy()
)
if kind == "SCALE":
scale *= value
else:
Expand Down
68 changes: 30 additions & 38 deletions act/back_end/layer_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,8 @@
# Will import at runtime when needed to avoid circular import
pass

try:
import torch
Tensor = torch.Tensor
except Exception: # typing only
Tensor = "torch.Tensor" # type: ignore
import torch
Tensor = torch.Tensor

# ------------------------------
# Strict validation & helpers
Expand Down Expand Up @@ -250,54 +247,49 @@ def create_layer(id: int, kind: str, params: Dict[str, Any],
# Tiny example (run file)
# ---------------------
if __name__ == "__main__":
try:
import torch # type: ignore
import sys
import os
# Add parent directory to path for absolute imports
from act.util.path_config import get_project_root
project_root = get_project_root()
sys.path.insert(0, project_root)
import sys
# Add parent directory to path for absolute imports
from act.util.path_config import get_project_root
project_root = get_project_root()
sys.path.insert(0, project_root)

from act.back_end.core import Layer
from act.back_end.layer_schema import LayerKind
from typing import List
layers: List[Layer] = []
from act.back_end.core import Layer
from act.back_end.layer_schema import LayerKind
from typing import List
layers: List[Layer] = []

# INPUT
layers.append(create_layer(
# INPUT
layers.append(create_layer(
id=0, kind=LayerKind.INPUT.value,
params={},
in_vars=[0], out_vars=[0],
))
# SPEC (directly after INPUT - no adapters)
lb_tensor = torch.full((1,3,32,32), -1.0)
ub_tensor = torch.full((1,3,32,32), 1.0)
layers.append(create_layer(
))
# SPEC (directly after INPUT - no adapters)
lb_tensor = torch.full((1,3,32,32), -1.0)
ub_tensor = torch.full((1,3,32,32), 1.0)
layers.append(create_layer(
id=1, kind=LayerKind.INPUT_SPEC.value,
params={"kind": InKind.BOX, "lb": lb_tensor, "ub": ub_tensor},
in_vars=[0], out_vars=[0],
))
# Model toy
layers.append(create_layer(
))
# Model toy
layers.append(create_layer(
id=2, kind=LayerKind.FLATTEN.value,
params={},
in_vars=[0], out_vars=[1],
))
W, b = torch.randn(10, 3072), torch.randn(10)
layers.append(create_layer(
))
W, b = torch.randn(10, 3072), torch.randn(10)
layers.append(create_layer(
id=3, kind=LayerKind.DENSE.value,
params={"W": W, "b": b},
in_vars=[1], out_vars=[2],
))
layers.append(create_layer(
))
layers.append(create_layer(
id=4, kind=LayerKind.ASSERT.value,
params={},
in_vars=[2], out_vars=[2],
))
))

validate_graph(layers)
validate_wrapper_graph(layers)
print("OK — wrapper model passes with", len(layers), "layers.")
except Exception as e:
print("Example failed:\n", e)
validate_graph(layers)
validate_wrapper_graph(layers)
print("OK — wrapper model passes with", len(layers), "layers.")
15 changes: 1 addition & 14 deletions act/back_end/net_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,6 @@ def append_binary_op(layers, *, op_kind: str, x_idx: int, y_idx: int) -> None:
)


def append_concat(layers, *, input_indices: List[int], concat_dim: int = 0) -> None:
_append_layer(
layers,
LayerKind.CONCAT.value,
params={"concat_dim": concat_dim},
preds=input_indices,
)


def append_flatten(layers) -> None:
_append_layer(layers, LayerKind.FLATTEN.value, params={"start_dim": 1})

Expand Down Expand Up @@ -1186,11 +1177,7 @@ def __init__(
self.total_generated = 0

def _compute_allowed_layers(self, tf_targets, mode):
try:
return _get_allowed_layers(tf_targets, mode)
except Exception as e:
logger.warning("TF capabilities unavailable: %s. Using defaults.", e)
return frozenset(_DEFAULT_COVERAGE_LAYERS)
return _get_allowed_layers(tf_targets, mode)

def _init_coverage(self):
skip = {
Expand Down
Loading
Loading