Skip to content

Commit 6e045bc

Browse files
Jason-LudmirJason Ludmirweinbe58
authored
perf(search): cache ConfigurationNode derived occupancy and lookup state (#334)
* perf(search): cache ConfigurationNode derived occupancy and lookup state Precompute configuration key, occupied locations, and location-to-qubit mappings in ConfigurationNode to avoid repeated recomputation in search hot paths. Add a stability regression test for repeated derived-view and lookup accesses. Made-with: Cursor * perf(search): cache lane indexes for source and triplet lookups (#327) Precompute lane indexes in ConfigurationTree to avoid rebuilding lane mappings in hot search loops. This adds constant-time helpers for triplet and source-based lane queries used during traversal and scoring. Made-with: Cursor Co-authored-by: Jason Ludmir <jasonludmir@JLudmir-Mac-QDNH.local> Co-authored-by: Phillip Weinberg <weinbe58@gmail.com> * perf(search): use cached_property for ConfigurationNode accessors Align ConfigurationNode caching with review guidance by using cached_property on derived accessors while preserving existing cached values. Made-with: Cursor --------- Co-authored-by: Jason Ludmir <jasonludmir@JLudmir-Mac-QDNH.local> Co-authored-by: Phillip Weinberg <weinbe58@gmail.com>
1 parent 2f454c6 commit 6e045bc

File tree

3 files changed

+119
-19
lines changed

3 files changed

+119
-19
lines changed

python/bloqade/lanes/search/configuration.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass, field
6+
from functools import cached_property
67

78
from bloqade.lanes.layout import LaneAddress, LocationAddress
89

@@ -37,30 +38,40 @@ class ConfigurationNode:
3738
depth: int = 0
3839
"""Distance from the root node."""
3940

40-
@property
41+
_config_key: Configuration = field(init=False, repr=False, compare=False)
42+
_occupied_locations: frozenset[LocationAddress] = field(
43+
init=False, repr=False, compare=False
44+
)
45+
_qubit_at_location: dict[LocationAddress, int] = field(
46+
init=False, repr=False, compare=False
47+
)
48+
49+
def __post_init__(self) -> None:
50+
self._config_key = frozenset(self.configuration.items())
51+
self._occupied_locations = frozenset(self.configuration.values())
52+
self._qubit_at_location = {loc: qid for qid, loc in self.configuration.items()}
53+
54+
@cached_property
4155
def config_key(self) -> Configuration:
4256
"""Canonical hashable key for this configuration.
4357
4458
Two nodes with the same atom placement (regardless of history)
4559
produce the same config_key.
4660
"""
47-
return frozenset(self.configuration.items())
61+
return self._config_key
4862

49-
@property
63+
@cached_property
5064
def occupied_locations(self) -> frozenset[LocationAddress]:
5165
"""The set of physical locations currently occupied by atoms."""
52-
return frozenset(self.configuration.values())
66+
return self._occupied_locations
5367

5468
def is_occupied(self, location: LocationAddress) -> bool:
5569
"""Check whether a physical location has an atom."""
56-
return location in self.configuration.values()
70+
return location in self._occupied_locations
5771

5872
def get_qubit_at(self, location: LocationAddress) -> int | None:
5973
"""Return the qubit ID at a location, or None if empty."""
60-
for qid, loc in self.configuration.items():
61-
if loc == location:
62-
return qid
63-
return None
74+
return self._qubit_at_location.get(location)
6475

6576
def path_to_root(self) -> list[frozenset[LaneAddress]]:
6677
"""Walk from this node to the root, returning move sets in root-to-leaf order.

python/bloqade/lanes/search/tree.py

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from collections import defaultdict
56
from collections.abc import Iterator
67
from dataclasses import dataclass, field
78
from typing import TYPE_CHECKING
@@ -41,10 +42,70 @@ class ConfigurationTree:
4142
seen: dict[Configuration, ConfigurationNode] = field(
4243
default_factory=dict, init=False, repr=False
4344
)
45+
_lanes_by_triplet: dict[
46+
tuple[MoveType, int, Direction], tuple[LaneAddress, ...]
47+
] = field(default_factory=dict, init=False, repr=False)
48+
_lane_by_src: dict[
49+
tuple[MoveType, int, Direction], dict[LocationAddress, LaneAddress]
50+
] = field(default_factory=dict, init=False, repr=False)
51+
_outgoing_lanes_by_src: dict[LocationAddress, tuple[LaneAddress, ...]] = field(
52+
default_factory=dict, init=False, repr=False
53+
)
4454

4555
def __post_init__(self) -> None:
4656
self.path_finder = PathFinder(self.arch_spec)
4757
self.seen[self.root.config_key] = self.root
58+
self._build_lane_indexes()
59+
60+
def _build_lane_indexes(self) -> None:
61+
"""Precomputes all lane mappings once (meant to be called at tree construction time)."""
62+
lanes_by_triplet: dict[tuple[MoveType, int, Direction], list[LaneAddress]] = {}
63+
lane_by_src: dict[
64+
tuple[MoveType, int, Direction], dict[LocationAddress, LaneAddress]
65+
] = {}
66+
outgoing: dict[LocationAddress, list[LaneAddress]] = defaultdict(list)
67+
68+
for mt in (MoveType.SITE, MoveType.WORD):
69+
buses = (
70+
self.arch_spec.site_buses
71+
if mt == MoveType.SITE
72+
else self.arch_spec.word_buses
73+
)
74+
for bus_id, bus in enumerate(buses):
75+
for direction in (Direction.FORWARD, Direction.BACKWARD):
76+
key = (mt, bus_id, direction)
77+
lanes_for_key: list[LaneAddress] = []
78+
src_map: dict[LocationAddress, LaneAddress] = {}
79+
if mt == MoveType.SITE:
80+
for word_id in self.arch_spec.has_site_buses:
81+
for site_id in bus.src:
82+
lane = LaneAddress(
83+
mt, word_id, site_id, bus_id, direction
84+
)
85+
src, _ = self.arch_spec.get_endpoints(lane)
86+
lanes_for_key.append(lane)
87+
src_map[src] = lane
88+
outgoing[src].append(lane)
89+
else:
90+
for word_id in bus.src:
91+
for site_id in self.arch_spec.has_word_buses:
92+
lane = LaneAddress(
93+
mt, word_id, site_id, bus_id, direction
94+
)
95+
src, _ = self.arch_spec.get_endpoints(lane)
96+
lanes_for_key.append(lane)
97+
src_map[src] = lane
98+
outgoing[src].append(lane)
99+
lanes_by_triplet[key] = lanes_for_key
100+
lane_by_src[key] = src_map
101+
102+
self._lanes_by_triplet = {
103+
key: tuple(values) for key, values in lanes_by_triplet.items()
104+
}
105+
self._lane_by_src = lane_by_src
106+
self._outgoing_lanes_by_src = {
107+
src: tuple(values) for src, values in outgoing.items()
108+
}
48109

49110
@classmethod
50111
def from_initial_placement(
@@ -80,16 +141,21 @@ def lanes_for(
80141
Yields:
81142
LaneAddress values.
82143
"""
83-
if move_type == MoveType.SITE:
84-
bus = self.arch_spec.site_buses[bus_id]
85-
for w in self.arch_spec.has_site_buses:
86-
for s in bus.src:
87-
yield LaneAddress(move_type, w, s, bus_id, direction)
88-
else:
89-
bus = self.arch_spec.word_buses[bus_id]
90-
for w in bus.src:
91-
for s in self.arch_spec.has_word_buses:
92-
yield LaneAddress(move_type, w, s, bus_id, direction)
144+
yield from self._lanes_by_triplet[(move_type, bus_id, direction)]
145+
146+
def lane_for_source(
147+
self,
148+
move_type: MoveType,
149+
bus_id: int,
150+
direction: Direction,
151+
source: LocationAddress,
152+
) -> LaneAddress | None:
153+
"""Resolve one lane by source for a specific triplet."""
154+
return self._lane_by_src[(move_type, bus_id, direction)].get(source)
155+
156+
def outgoing_lanes(self, source: LocationAddress) -> tuple[LaneAddress, ...]:
157+
"""Return all precomputed outgoing lanes from source."""
158+
return self._outgoing_lanes_by_src.get(source, ())
93159

94160
def valid_lanes(
95161
self,

python/tests/search/test_configuration.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,29 @@ def test_occupied_locations():
7777
assert node.occupied_locations == frozenset({loc0, loc1})
7878

7979

80+
def test_cached_derived_views_and_lookup_methods_are_stable():
81+
loc0 = LocationAddress(0, 0)
82+
loc1 = LocationAddress(1, 0)
83+
empty_loc = LocationAddress(0, 5)
84+
node = ConfigurationNode(configuration={0: loc0, 1: loc1})
85+
86+
first_key = node.config_key
87+
first_occupied = node.occupied_locations
88+
89+
# Repeated access should return stable cached objects.
90+
assert node.config_key is first_key
91+
assert node.occupied_locations is first_occupied
92+
93+
# Lookup helpers should remain stable across repeated calls.
94+
for _ in range(3):
95+
assert node.get_qubit_at(loc0) == 0
96+
assert node.get_qubit_at(loc1) == 1
97+
assert node.get_qubit_at(empty_loc) is None
98+
assert node.is_occupied(loc0) is True
99+
assert node.is_occupied(loc1) is True
100+
assert node.is_occupied(empty_loc) is False
101+
102+
80103
def test_path_to_root_at_root():
81104
node = ConfigurationNode(configuration={0: LocationAddress(0, 0)})
82105
assert node.path_to_root() == []

0 commit comments

Comments
 (0)