mirror of
https://github.com/luxfhe/workshop.git
synced 2026-10-30 17:06:44 +00:00
192 lines
6.6 KiB
Python
192 lines
6.6 KiB
Python
"""Graph pre-processor for automatic rounding.
|
|
"""
|
|
|
|
from copy import deepcopy
|
|
from typing import Callable, Dict, Optional, Union
|
|
|
|
import networkx as nx
|
|
import numpy as np
|
|
from torus.fhe import Exactness, round_bit_pattern
|
|
from torus.fhe.dtypes import Integer
|
|
from torus.fhe.representation import Graph, GraphProcessor, Node, Operation
|
|
|
|
|
|
def is_node_tlu(node: Node) -> bool:
|
|
"""Determine if a graph node is a table lookup.
|
|
|
|
Args:
|
|
node (Node): graph node to check
|
|
|
|
Returns:
|
|
bool: boolean indicating whether the node is a TLU
|
|
"""
|
|
return node.converted_to_table_lookup
|
|
|
|
|
|
def bit_width(x):
|
|
return Integer.that_can_represent(x).bit_width
|
|
|
|
|
|
def add_rounding_node(
|
|
a_node: Node,
|
|
lsbs_to_remove: int,
|
|
graph: nx.DiGraph,
|
|
rounding_function: Callable = round_bit_pattern,
|
|
exactness=Exactness.EXACT,
|
|
overflow_protection: bool = False,
|
|
) -> Node:
|
|
"""Modify a computation graph to include a rounding node.
|
|
|
|
Args:
|
|
a_node (Node): the node whose output will be rounded
|
|
lsbs_to_remove (int): the number of least significant bits to remove
|
|
graph (nx.DiGraph): the graph containing the node
|
|
rounding_function (Callable): the function to use for rounding
|
|
exactness: FHE rounding mode, either Exactness.EXACT or Exactness.APPROXIMATE
|
|
overflow_protection (bool): use FHE overflow protection
|
|
|
|
Returns:
|
|
Node: the rounding node that was added to the graph
|
|
"""
|
|
|
|
if lsbs_to_remove <= 0:
|
|
# No rounding node to add
|
|
return a_node
|
|
|
|
# Sanity check and mypy check
|
|
assert isinstance(a_node.output.dtype, Integer)
|
|
|
|
# Adding rounding node
|
|
rounding_kwargs: Dict[str, Union[Exactness, int, bool]] = {
|
|
"lsbs_to_remove": lsbs_to_remove,
|
|
"overflow_protection": overflow_protection,
|
|
}
|
|
attributes = {
|
|
"overflow_protection": overflow_protection,
|
|
}
|
|
|
|
# Only round_bit_pattern support exactness for now
|
|
if rounding_function.__name__ == "round_bit_pattern":
|
|
rounding_kwargs["exactness"] = exactness
|
|
|
|
output_value = deepcopy(a_node.output)
|
|
new_bounds_arr = rounding_function(np.array(a_node.bounds, dtype=np.int64), **rounding_kwargs)
|
|
output_value.dtype = Integer.that_can_represent(new_bounds_arr)
|
|
|
|
rounding_node = Node.generic(
|
|
name=rounding_function.__name__,
|
|
inputs=[deepcopy(a_node.output)],
|
|
output=output_value,
|
|
operation=rounding_function,
|
|
kwargs=rounding_kwargs,
|
|
attributes=attributes,
|
|
)
|
|
rounding_node.properties["final_lsbs_to_remove"] = lsbs_to_remove
|
|
rounding_node.properties["overflow_detected"] = False
|
|
rounding_node.properties["original_rounded_bit_width"] = a_node.output.dtype.bit_width
|
|
rounding_node.properties["overflow_protection"] = overflow_protection
|
|
|
|
# Compute new bounds and bit-width
|
|
assert a_node.bounds is not None
|
|
rounding_node.bounds = (new_bounds_arr[0], new_bounds_arr[1])
|
|
rounding_node.properties["resulting_bit_width"] = output_value.dtype.bit_width
|
|
if output_value.dtype.bit_width > a_node.output.dtype.bit_width:
|
|
rounding_node.properties["overflow_detected"] = True
|
|
|
|
# Add edge between node and rounding node
|
|
graph.add_edge(a_node, rounding_node, input_idx=0)
|
|
|
|
# Replace a -> o_i by rounding_node -> o_i
|
|
edges = list(graph.out_edges(nbunch=a_node)) # type: ignore
|
|
for in_node, out_node in edges:
|
|
if out_node == rounding_node:
|
|
continue
|
|
|
|
# We should preserve the input_idx
|
|
edge_data: Dict[int, Dict[str, int]] = dict(graph.get_edge_data(in_node, out_node))
|
|
graph.remove_edge(in_node, out_node)
|
|
input_idx: int = edge_data[0]["input_idx"]
|
|
graph.add_edge(rounding_node, out_node, input_idx=input_idx)
|
|
|
|
return rounding_node
|
|
|
|
|
|
class InsertRounding(GraphProcessor):
|
|
"""
|
|
InsertRounding graph processor, to add rounding before TLUs if desired.
|
|
"""
|
|
|
|
rounding_threshold: Optional[int]
|
|
|
|
def __init__(
|
|
self,
|
|
msbs_to_keep: Optional[int],
|
|
exactness: Exactness = Exactness.APPROXIMATE,
|
|
overflow_protection: bool = True,
|
|
rounding_function=round_bit_pattern,
|
|
):
|
|
self.rounding_threshold = msbs_to_keep
|
|
self.exactness = exactness
|
|
self.overflow_protection = overflow_protection
|
|
self.rounding_function = rounding_function
|
|
assert self.rounding_function.__name__ in {"round_bit_pattern", "truncate_bit_pattern"}
|
|
if self.rounding_function.__name__ == "truncate_bit_pattern":
|
|
assert exactness == Exactness.EXACT
|
|
|
|
def apply(self, graph: Graph):
|
|
if self.rounding_threshold is None:
|
|
# No rounding if None
|
|
return
|
|
|
|
# Get all nodes that will be converted to LUTs
|
|
tlu_nodes = graph.query_nodes(
|
|
custom_filter=is_node_tlu,
|
|
ordered=True,
|
|
)
|
|
|
|
for tlu_node in tlu_nodes:
|
|
# Predecessor nodes of LUT node
|
|
pred_nodes = graph.ordered_preds_of(tlu_node)
|
|
|
|
# Only take into accound predecessor's that aren't constants
|
|
variable_input_indices = []
|
|
for pred_index, pred_node in enumerate(pred_nodes):
|
|
if pred_node.operation != Operation.Constant:
|
|
variable_input_indices.append(pred_index)
|
|
|
|
# Only one input should be non-constant per LUT
|
|
if len(variable_input_indices) != 1:
|
|
continue
|
|
|
|
pred_node = pred_nodes[variable_input_indices[0]]
|
|
|
|
# Continue if the predecessor node is rounding node
|
|
if pred_node.properties["name"] in {"round_bit_pattern", "truncate_bit_pattern"}:
|
|
continue
|
|
|
|
# Continue if the node itself is a rounding node
|
|
if tlu_node.properties["name"] in {"round_bit_pattern", "truncate_bit_pattern"}:
|
|
continue
|
|
|
|
# Sanity check
|
|
if not isinstance(pred_node.output.dtype, Integer):
|
|
raise ValueError(f"{pred_node.output.dtype=} is not 'Integer'")
|
|
|
|
if pred_node.output.dtype.bit_width <= self.rounding_threshold:
|
|
# No need to do anything if the bit-width is actually lower or equal
|
|
# to the rounding threshold value
|
|
continue
|
|
|
|
# Compute lsbs to remove
|
|
lsbs_to_remove = pred_node.output.dtype.bit_width - self.rounding_threshold
|
|
|
|
# Add rounding node
|
|
add_rounding_node(
|
|
pred_node,
|
|
lsbs_to_remove=lsbs_to_remove,
|
|
graph=graph.graph,
|
|
rounding_function=self.rounding_function,
|
|
exactness=self.exactness,
|
|
overflow_protection=self.overflow_protection,
|
|
)
|