Skip to content

Add nfa_frontier: the ANMLZoo/VASim homogeneous-NFA simulation cycle - #20

Open
laesse10 wants to merge 2 commits into
mainfrom
anmlzoo-nfa
Open

Add nfa_frontier: the ANMLZoo/VASim homogeneous-NFA simulation cycle#20
laesse10 wants to merge 2 commits into
mainfrom
anmlzoo-nfa

Conversation

@laesse10

Copy link
Copy Markdown
Collaborator

Ported from VASim (github.com/jackwadden/VASim, MIT), the reference engine of the ANMLZoo automata-processing suite (github.com/jackwadden/ANMLZoo, IISWC'16).

Where the time goes. VASim links no CUDA at all -- an nsys probe over a whole run records zero kernels and zero device memory ops -- so this is the perf path. perf record -g --call-graph=dwarf -F 999 -e cycles:u on four ANMLZoo automata (Brill 10 MB / 780 s / 774K samples, Fermi 1 MB / 463 s / 459K, EntityResolution 10 MB / 489 s / 486K, Snort 10 MB / 901 s / 896K) puts 99.9% of user cycles inside Automata::simulate(uint8_t) on the three STE-only automata, with the same split every time: enableChildSTEs 48-68%, computeSTEMatches 27-33%, enableStartStates 1-13%. There is no library leaf to walk out of -- the hot code is VASim's own per-symbol cycle -- so that function is the boundary.

The kernel keeps upstream's worklist form (a frontier of state indices plus a per-state dedup flag) and its CSR successor walk: the irregular gather/scatter and the data-dependent trip counts are the point. It drops upstream's per-edge pair<Element*, string> copy, which the profile shows costs 23% of the whole run inside enableChildSTEs, since that is a container choice rather than automata mathematics.

Fidelity is checked against the application, not only against a checker. Fed VASim's own parse of Levenshtein, Brill, Snort and Fermi, the kernel reproduces VASim's activation histogram state for state -- 2,284,315 / 6,611,433 / 1,699,583 / 4,342,794 activations, zero mismatched states -- and its report count exactly on all but Snort, whose 708 counter/gate elements the port deliberately omits (8722 vs 8917); on that automaton VASim spends 62% of the run in the special-element step, almost all of it walking a std::map<std::string, bool> by value rather than evaluating gates. Two independent formulations (a dense boolean adjacency matvec beside the kernel, a set-and-dict walk in tests/test_ported_references.py) pin the recurrence without reusing its data structures.

Sizing comes from the same measurements: the generated automaton lands inside ANMLZoo's measured bands for fan-out, symbol-set density, self-loops, start states and -- the number that decides the per-cycle work -- mean active set.

nfa_frontier joins numerical_oracle.NO_SCALE because NS/NE/NSTART are three views of one automaton; scaling them independently asks for a graph that does not exist.

Ported from VASim (github.com/jackwadden/VASim, MIT), the reference engine of the
ANMLZoo automata-processing suite (github.com/jackwadden/ANMLZoo, IISWC'16).

Where the time goes. VASim links no CUDA at all -- an nsys probe over a whole run
records zero kernels and zero device memory ops -- so this is the perf path.
`perf record -g --call-graph=dwarf -F 999 -e cycles:u` on four ANMLZoo automata
(Brill 10 MB / 780 s / 774K samples, Fermi 1 MB / 463 s / 459K, EntityResolution
10 MB / 489 s / 486K, Snort 10 MB / 901 s / 896K) puts 99.9% of user cycles inside
`Automata::simulate(uint8_t)` on the three STE-only automata, with the same split
every time: enableChildSTEs 48-68%, computeSTEMatches 27-33%, enableStartStates
1-13%. There is no library leaf to walk out of -- the hot code is VASim's own
per-symbol cycle -- so that function is the boundary.

The kernel keeps upstream's worklist form (a frontier of state indices plus a
per-state dedup flag) and its CSR successor walk: the irregular gather/scatter and
the data-dependent trip counts are the point. It drops upstream's per-edge
`pair<Element*, string>` copy, which the profile shows costs 23% of the whole run
inside enableChildSTEs, since that is a container choice rather than automata
mathematics.

Fidelity is checked against the application, not only against a checker. Fed
VASim's own parse of Levenshtein, Brill, Snort and Fermi, the kernel reproduces
VASim's activation histogram state for state -- 2,284,315 / 6,611,433 / 1,699,583 /
4,342,794 activations, zero mismatched states -- and its report count exactly on
all but Snort, whose 708 counter/gate elements the port deliberately omits (8722 vs
8917); on that automaton VASim spends 62% of the run in the special-element step,
almost all of it walking a `std::map<std::string, bool>` by value rather than
evaluating gates. Two independent formulations (a dense boolean adjacency matvec beside the
kernel, a set-and-dict walk in tests/test_ported_references.py) pin the recurrence
without reusing its data structures.

Sizing comes from the same measurements: the generated automaton lands inside
ANMLZoo's measured bands for fan-out, symbol-set density, self-loops, start states
and -- the number that decides the per-cycle work -- mean active set.

nfa_frontier joins numerical_oracle.NO_SCALE because NS/NE/NSTART are three views
of one automaton; scaling them independently asks for a graph that does not exist.
…ng sympy

Two defects in the first cut of this port, both found by driving CI's own phases
against the kernel locally rather than reading the red checks on the PR.

The port hid the parallelism it was supposed to expose. VASim's per-symbol cycle is
a strict recurrence, but the application does not thread over symbols -- it threads
over the graph: `splitConnectedComponents` cuts the automaton into independent
components, distributes them among `-T` threads, and each thread runs the whole
stream through its own. That axis is real and wide in ANMLZoo: 24 components in
Levenshtein, 1962 in Brill, 2687 in Snort, 2399 in Fermi. The first version
flattened every component into one CSR and one frontier, so an optimizer saw a
single sequential worklist and the map was gone.

The kernel is now that map. `for c in range(C)` walks the components; `comp_ptr[c]
.. comp_ptr[c+1]` gives each one a disjoint slice of the shared `enabled`,
`frontier` and `matched` buffers, its own states in `activation_counts`, and its own
`report_counts[c]` -- so the map needs neither privatisation nor a reduction, and the
recurrence stays exactly where upstream has it, inside a component. Components are
deliberately non-uniform (21/33/45 states, cycled) because ANMLZoo's are: Brill's
median component is 21 states against a largest of 67, so an equal-size static
schedule over `c` leaves cores idle, and that imbalance belongs in the benchmark.

The stream argument was named `symbols`, and `sympy.symbols` is a function. DaCe's
`scalar_to_symbol` pass -- which runs only under `simplify=True`, so a clean
`simplify=False` parse probe said nothing -- sympifies loop-edge expressions in a
namespace where that name resolves to the function, and the numeric gate failed with
`SympifyError: cannot sympify object of type <class 'function'>`. Renamed to
`stream`; of the kernel's 31 identifiers it was the only one that resolved to a
sympy callable.

Re-verified after both changes: VASim's activation histogram still agrees state for
state on Levenshtein, Brill, Snort and Fermi (2,284,315 / 6,611,433 / 1,699,583 /
4,342,794 activations, zero mismatched states), which also demonstrates that running
the components independently computes what running their union computes. The
benchmark-local test now checks the worklist map against a dense boolean matvec over
the WHOLE graph, so the decomposition itself is under test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant