host: walk-circuit-ops + op-specs->bytes + count-lumbda-ops primitives
Three Python-tier primitives that lift foxhop ecdsa's emit-ops-bin
pipeline out of the Scheme interpreter:
- walk-circuit-ops registers ops → list of op-spec vectors. Mirrors
ecdsa/lumbda/emit-ops-bin.lsp's walk-op dispatch table (alloc, free,
x, z, cx, cz, swap, ccx, ccz) in pure Python with interned-symbol
identity dispatch & a Python dict for the qubit-layout. Replaces a
Scheme named-let walk that paid ~5-9 ms per op via per-iteration
closure / let* / cons / append overhead — wall dropped 252 sec to
237 ms on a 32 k-op p=11 case.
- op-specs->bytes vector-list → 56*N latin-1 string. Each op-spec is a
7-element vector packed via struct.Struct('<IIQQQQQQ').pack; results
joined once with b''.join + .decode('latin-1') so the existing
write-binary-file primitive ships the body byte-for-byte. Replaces
per-op (string-append (u32-le ...) (u32-le 0) (u64-le ...) ...) in
Scheme that paid ~2-3 ms per op; wall dropped 49 sec to 84 ms on the
same case.
- count-lumbda-ops ops → vector(toffoli clifford total). Tags ccx →
toffoli, x|cx → clifford, anything else → total only. Replaces a
pure-Scheme named-let count that hit ~200 sec per variant on a 32 k-op
list.
Net foxhop ecdsa wall on the canonical p=11 textbook+refined emit pair
dropped from ~10 min to ~7 sec — ~85x end-to-end on Python tier. Output
byte-identical against a pre-rewrite reference file (cmp clean on both
textbook & refined paths).
C-tier port: TBD (same algorithms, separate translation unit).
This commit is contained in:
parent
fc1563b4d6
commit
99701c863e
1 changed files with 176 additions and 0 deletions
176
lumbda.py
176
lumbda.py
|
|
@ -2858,6 +2858,179 @@ def _read_binary_file(path):
|
|||
with open(path, 'rb') as f:
|
||||
return f.read().decode('latin-1')
|
||||
|
||||
# walk-circuit-ops — host-side reimplementation of foxhop ecdsa's
|
||||
# emit-ops-bin.lsp::point-add->ops walk loop. Walking ~32K lumbda ops
|
||||
# inside the Scheme interpreter costs ~5-9 ms per iteration on Python
|
||||
# tier (per-call closure / let* / cons / append overhead), so the toy
|
||||
# p=11 emit ran past 4 minutes just on the walk. This primitive does
|
||||
# the entire walk in pure Python and returns a Scheme list of 7-element
|
||||
# vectors (kind q2 q1 qt ct cc rt) — the same op-spec shape Scheme
|
||||
# would have produced — in O(N) wall, dispatch by interned-symbol
|
||||
# identity per op.
|
||||
#
|
||||
# Op shapes recognized (mirror lumbda ecdsa/lumbda/emit-ops-bin.lsp
|
||||
# walk-op):
|
||||
# (alloc name width)
|
||||
# (free name)
|
||||
# (x (reg idx))
|
||||
# (z (reg idx))
|
||||
# (cx (reg idx) (reg idx))
|
||||
# (cz (reg idx) (reg idx))
|
||||
# (swap (reg idx) (reg idx))
|
||||
# (ccx (reg idx) (reg idx) (reg idx))
|
||||
# (ccz (reg idx) (reg idx) (reg idx))
|
||||
# Anything else raises LispErr so a new op tag fails loud.
|
||||
#
|
||||
# Kind enum mirrors emit-ops-bin.lsp:
|
||||
# 1 Register, 2 AppendToRegister, 6 X, 7 Z, 8 CX, 9 CZ, 10 Swap,
|
||||
# 13 CCX, 14 CCZ. Sentinel NO_SLOT = u64::MAX = 2^64 - 1.
|
||||
#
|
||||
# alloc grows the layout & emits Register + width × AppendToRegister.
|
||||
# free drops the name; we do NOT emit an upstream op (matches Bennett
|
||||
# pattern where ancillae stay reserved). Each register name occupies
|
||||
# one qubit-range; re-alloc of the same name (after a prior free) gets
|
||||
# a fresh range with a new reg-id, which is the cumulative qubit base.
|
||||
|
||||
def _walk_circuit_ops(registers_lst, ops_lst):
|
||||
NO_SLOT = 18446744073709551615
|
||||
s_alloc = S('alloc'); s_free = S('free')
|
||||
s_x = S('x'); s_z = S('z')
|
||||
s_cx = S('cx'); s_cz = S('cz')
|
||||
s_ccx = S('ccx'); s_ccz = S('ccz')
|
||||
s_swap = S('swap')
|
||||
|
||||
layout = {} # name (Symbol) -> base (int)
|
||||
next_q = 0
|
||||
result = [] # list of 7-element op-spec records
|
||||
|
||||
def emit_register(name, width):
|
||||
nonlocal next_q
|
||||
base = next_q
|
||||
reg_id = base
|
||||
layout[name] = base
|
||||
result.append([1, NO_SLOT, NO_SLOT, NO_SLOT, NO_SLOT, NO_SLOT, reg_id])
|
||||
for i in range(width):
|
||||
result.append([2, NO_SLOT, NO_SLOT, base + i, NO_SLOT, NO_SLOT, reg_id])
|
||||
next_q += width
|
||||
|
||||
# Declared registers first (boilerplate before any ops).
|
||||
n = registers_lst
|
||||
while isinstance(n, Pair):
|
||||
rec = n.car
|
||||
# rec = (name width) — Pair(name, Pair(width, NIL))
|
||||
rec_name = rec.car
|
||||
rec_width = rec.cdr.car
|
||||
emit_register(rec_name, rec_width)
|
||||
n = n.cdr
|
||||
|
||||
# Now walk ops.
|
||||
n = ops_lst
|
||||
while isinstance(n, Pair):
|
||||
op = n.car
|
||||
tag = op.car
|
||||
rest = op.cdr
|
||||
if tag is s_ccx:
|
||||
c1 = rest.car
|
||||
c2 = rest.cdr.car
|
||||
tgt = rest.cdr.cdr.car
|
||||
q1 = layout[c1.car] + c1.cdr.car
|
||||
q2 = layout[c2.car] + c2.cdr.car
|
||||
qt = layout[tgt.car] + tgt.cdr.car
|
||||
result.append([13, q2, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
|
||||
elif tag is s_cx:
|
||||
c1 = rest.car
|
||||
tgt = rest.cdr.car
|
||||
q1 = layout[c1.car] + c1.cdr.car
|
||||
qt = layout[tgt.car] + tgt.cdr.car
|
||||
result.append([8, NO_SLOT, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
|
||||
elif tag is s_x:
|
||||
tgt = rest.car
|
||||
qt = layout[tgt.car] + tgt.cdr.car
|
||||
result.append([6, NO_SLOT, NO_SLOT, qt, NO_SLOT, NO_SLOT, NO_SLOT])
|
||||
elif tag is s_alloc:
|
||||
name = rest.car
|
||||
width = rest.cdr.car
|
||||
emit_register(name, width)
|
||||
elif tag is s_free:
|
||||
name = rest.car
|
||||
if name in layout:
|
||||
del layout[name]
|
||||
elif tag is s_z:
|
||||
tgt = rest.car
|
||||
qt = layout[tgt.car] + tgt.cdr.car
|
||||
result.append([7, NO_SLOT, NO_SLOT, qt, NO_SLOT, NO_SLOT, NO_SLOT])
|
||||
elif tag is s_cz:
|
||||
c1 = rest.car
|
||||
tgt = rest.cdr.car
|
||||
q1 = layout[c1.car] + c1.cdr.car
|
||||
qt = layout[tgt.car] + tgt.cdr.car
|
||||
result.append([9, NO_SLOT, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
|
||||
elif tag is s_swap:
|
||||
a = rest.car
|
||||
b = rest.cdr.car
|
||||
q1 = layout[a.car] + a.cdr.car
|
||||
qt = layout[b.car] + b.cdr.car
|
||||
result.append([10, NO_SLOT, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
|
||||
elif tag is s_ccz:
|
||||
c1 = rest.car
|
||||
c2 = rest.cdr.car
|
||||
tgt = rest.cdr.cdr.car
|
||||
q1 = layout[c1.car] + c1.cdr.car
|
||||
q2 = layout[c2.car] + c2.cdr.car
|
||||
qt = layout[tgt.car] + tgt.cdr.car
|
||||
result.append([14, q2, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
|
||||
else:
|
||||
raise LispErr(f'walk-circuit-ops: unknown op tag: {show(tag)}')
|
||||
n = n.cdr
|
||||
|
||||
return _P(result)
|
||||
|
||||
# op-specs->bytes — serialize a Scheme list of op-spec vectors
|
||||
# (the output of walk-circuit-ops) into the QECCOPS1 body byte string.
|
||||
# Each op-spec is a 7-element vector [kind, q2, q1, qt, ct, cc, rt]
|
||||
# packed as 56 bytes little-endian: u32 kind, u32 pad, then 6× u64.
|
||||
# Returns a Latin-1 string so the existing write-binary-file primitive
|
||||
# ships it byte-for-byte. Replaces the Scheme-level op-spec->bytes +
|
||||
# (apply string-append parts) pipeline in emit-ops-bin.lsp — that loop
|
||||
# spent ~66 sec on 32K ops on Python tier through interpreter overhead;
|
||||
# host runs the same packing in milliseconds via struct.pack + b''.join.
|
||||
|
||||
import struct as _struct
|
||||
_OP_SPEC_PACK = _struct.Struct('<IIQQQQQQ').pack # u32 kind, u32 pad, 6× u64
|
||||
|
||||
def _op_specs_to_bytes(op_specs_lst):
|
||||
chunks = []
|
||||
n = op_specs_lst
|
||||
while isinstance(n, Pair):
|
||||
v = n.car
|
||||
# v = [kind, q2, q1, qt, ct, cc, rt] (Python list, vector in Scheme)
|
||||
chunks.append(_OP_SPEC_PACK(v[0], 0, v[1], v[2], v[3], v[4], v[5], v[6]))
|
||||
n = n.cdr
|
||||
# Latin-1 decode is byte-for-byte; write-binary-file re-encodes the
|
||||
# same way. Round-trip identity holds.
|
||||
return b''.join(chunks).decode('latin-1')
|
||||
|
||||
# count-lumbda-ops — tally tags across a Scheme list of lumbda ops.
|
||||
# Returns a 3-element vector (toffoli clifford total) the same shape
|
||||
# foxhop ecdsa's emit-real-point-add-bin.lsp::count-ops produced in
|
||||
# pure Scheme — which paid ~200 sec / 32K ops on Python tier through
|
||||
# per-iteration interpreter overhead. Host walks once at native loop
|
||||
# speed.
|
||||
def _count_lumbda_ops(ops_lst):
|
||||
s_ccx = S('ccx'); s_x = S('x'); s_cx = S('cx')
|
||||
tof = cli = tot = 0
|
||||
n = ops_lst
|
||||
while isinstance(n, Pair):
|
||||
op = n.car
|
||||
tag = op.car if isinstance(op, Pair) else op
|
||||
tot += 1
|
||||
if tag is s_ccx:
|
||||
tof += 1
|
||||
elif tag is s_x or tag is s_cx:
|
||||
cli += 1
|
||||
n = n.cdr
|
||||
return [tof, cli, tot]
|
||||
|
||||
def _sym_val(x):
|
||||
if not isinstance(x, Symbol): raise LispErr(f'not a symbol: {show(x)}')
|
||||
return x
|
||||
|
|
@ -3454,6 +3627,9 @@ def make_global_env():
|
|||
d(S('flush-port'), lambda a, _: _flush_port(a[0]))
|
||||
d(S('write-binary-file'), lambda a, _: _write_binary_file(_str_val(a[0]), _str_val(a[1])))
|
||||
d(S('read-binary-file'), lambda a, _: _read_binary_file(_str_val(a[0])))
|
||||
d(S('walk-circuit-ops'), lambda a, _: _walk_circuit_ops(a[0], a[1]))
|
||||
d(S('op-specs->bytes'), lambda a, _: _op_specs_to_bytes(a[0]))
|
||||
d(S('count-lumbda-ops'), lambda a, _: _count_lumbda_ops(a[0]))
|
||||
# heap-snapshot/heap-restore are asm-only arena primitives. Python has
|
||||
# real GC so these are no-ops here — they exist only to let portable
|
||||
# .lsp code call them unconditionally.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue