Skip to content

Commit b6ceece

Browse files
authored
ensure applicable symbolic-accepting compilers are included (#669)
When we changed the way occ-tbl metrics were calculated, it caused quilc to find particularly good collections of compilers ("compiler paths") for certain gate sets. These particularly good compiler paths were better than before! But there was a hitch... the better compilers were also less flexible in what they matched against, though that wasn't apparent from the binding patterns themselves. In particular, in some cases, the better compilers were ones that also did _not_ accept symbolic parameters (e.g., a memory reference). This means these better compilers failed to take into account inputs like `RX(theta) 0` even though the pattern `RX(_) _` was compilable. The fix in this commit has two facets: - First, we use heuristics to determine if a compiler can accept symbolic parameters. This heuristic can be improved, but works fine for one-parameter compilers, which are the real-world cases that failed. - Second, even if we find a better compiler path when warming the chip spec, as long as a symbolic-accepting path was found, it's included in the list of compilers. This may still fail, if the entire path doesn't allow symbolic compilers. This was too complicated to check for this commit with too little gain. This fixes both issues reported in issue #667.
1 parent 909cda3 commit b6ceece

File tree

2 files changed

+158
-10
lines changed

2 files changed

+158
-10
lines changed

src/define-compiler.lisp

+119-10
Original file line numberDiff line numberDiff line change
@@ -353,15 +353,35 @@ What \"concrete\" means depends on the types of A and B:
353353
(:method ((a wildcard-binding) (b gate-binding))
354354
b))
355355

356+
(defvar *instantiate-binding-with-symbolic-parameters* nil
357+
"When calling INSTANTIATE-BINDING and encountering a symbolic parameter, instead of erroring, supply a symbolic parameter.")
358+
359+
(defun symbolic-param-generator ()
360+
(let ((n -1))
361+
(lambda ()
362+
(make-delayed-expression nil nil `(* 1.0 ,(mref "G" (incf n)))))))
363+
364+
(defun instantiate-parameters (params)
365+
;; Instantiate PARAMS with symbolic arguments. Used as a helper
366+
;; below.
367+
(let ((gen (symbolic-param-generator)))
368+
(mapcar (lambda (p)
369+
(if (symbolp p)
370+
(funcall gen)
371+
p))
372+
params)))
373+
356374
(defgeneric instantiate-binding (binding)
357375
(:documentation "When possible, construct the unique instruction on which BINDING will match. If any of the binding's arguments are unspecified (i.e. match against any qubit), those arguments will be filled-in with unique qubit indices.")
358376
(:method (binding)
359377
(error 'cannot-concretize-binding))
360378
(:method ((binding gate-binding))
361379
(when (symbolp (gate-binding-operator binding))
362380
(error 'cannot-concretize-binding))
363-
(when (or (symbolp (gate-binding-parameters binding))
364-
(some #'symbolp (gate-binding-parameters binding)))
381+
(when (symbolp (gate-binding-parameters binding))
382+
(error 'cannot-concretize-binding))
383+
(when (and (some #'symbolp (gate-binding-parameters binding))
384+
(not *instantiate-binding-with-symbolic-parameters*))
365385
(error 'cannot-concretize-binding))
366386
(when (some #'symbolp (gate-binding-arguments binding))
367387
(let* ((usedq (gate-binding-arguments binding))
@@ -371,7 +391,7 @@ What \"concrete\" means depends on the types of A and B:
371391
usedq)))))
372392
(apply #'build-gate
373393
(gate-binding-operator binding)
374-
(gate-binding-parameters binding)
394+
(instantiate-parameters (gate-binding-parameters binding))
375395
(gate-binding-arguments binding))))
376396

377397

@@ -405,6 +425,15 @@ What \"concrete\" means depends on the types of A and B:
405425
:initarg :output-gates
406426
:reader compiler-output-gates
407427
:documentation "Information automatically extracted about the target gate set.")
428+
(info-plist
429+
:initform nil
430+
:accessor compiler-info-plist
431+
:documentation "A plist of inessential information. You will likely find cached data here. Data includes:
432+
433+
:SYMBOLIC <boolean>
434+
Accepts symbolic parameters.
435+
436+
N.B. This is *NOT* the same as COMPILER-OPTIONS.")
408437
(%function
409438
:initarg :function
410439
:reader compiler-%function
@@ -668,6 +697,75 @@ N.B.: The word \"shortest\" here is a bit fuzzy. In practice it typically means
668697
(compiler-bindings x)))
669698
(get-compilers qubit-count)))
670699

700+
(defun compiler-allows-symbolic-parameters-p (compiler)
701+
"Does COMPILER accept instructions with symbolic parameters?
702+
703+
Compilers matching gates which don't have parameters don't count as accepting parameters."
704+
(let ((info (getf (compiler-info-plist compiler) ':SYMBOLIC '#1=#:unknown)))
705+
(if (eq info '#1#)
706+
(setf (getf (compiler-info-plist compiler) ':SYMBOLIC)
707+
(heuristically-determine-if-compiler-allows-symbolic-parameters-p compiler))
708+
info)))
709+
710+
(defun heuristically-determine-if-compiler-allows-symbolic-parameters-p (compiler)
711+
;; Things marked as "HEURISTIC ALERT" are not perfect tests, but
712+
;; good enough in practice.
713+
(block nil
714+
(let* ((bindings (compiler-bindings compiler)))
715+
;; All bindings must be gate bindings.
716+
(unless (every (lambda (b) (typep b 'gate-binding)) bindings)
717+
(return nil))
718+
;; Now we know we can unpack the bindings.
719+
;;
720+
;; HEURISTIC ALERT: Before we do that, we'll check we are
721+
;; actually operating on named gates. One could imagine the
722+
;; compilation of symbolic 2Q operators with wildcard 1Q gates
723+
;; surrounding, but we don't have this kind of thing anywhere.
724+
(unless (every (lambda (b) (typep (gate-binding-operator b) 'operator-description )) bindings)
725+
(return nil))
726+
;; Now we descend to look at the bindings' parameter lists for
727+
;; heuristic conformance.
728+
(let ((parameter-lists (map 'list #'gate-binding-parameters bindings)))
729+
;; HEURISTIC ALERT: All bindings have fewer than two
730+
;; parameters. This rule doesn't necessarily mean a compiler
731+
;; doesn't accept symbolic parameters, but it is unlikely in
732+
;; practice.
733+
;;
734+
;; We could also relax this rule by allowing bindings with
735+
;; any number of constant parameters. A fish to fry another
736+
;; day.
737+
(unless (every (lambda (p) (and (alexandria:proper-list-p p)
738+
(= 1 (length p))))
739+
parameter-lists)
740+
(return nil))
741+
;; Check that we have at least one non-constant parameter.
742+
(unless (some (lambda (p) (symbolp (first p))) parameter-lists)
743+
(return nil))
744+
;; Ok, so, here's what we now know:
745+
;;
746+
;; - All bindings take 1 parameter
747+
;;
748+
;; - At least one binding takes an arbitrary parameter.
749+
(let ((*instantiate-binding-with-symbolic-parameters* t))
750+
(let ((prototype-instructions (map 'list #'instantiate-binding bindings)))
751+
;; We are ready to lock & load.
752+
(handler-bind ((compiler-does-not-apply
753+
(lambda (c)
754+
(declare (ignore c))
755+
(return nil)))
756+
(error
757+
(lambda (c)
758+
(declare (ignore c))
759+
;; Decline to handle, but issue a warning.
760+
(warn "When checking if the compiler ~A accepts ~
761+
symbolic parameters, an unexpected error ~
762+
occurred. This is probably because this ~
763+
compiler matches against inputs it's not ~
764+
actually able to process, like symbolic ~
765+
expressions." (compiler-name compiler)))))
766+
(apply compiler prototype-instructions) ; May return an empty list!
767+
t)))))))
768+
671769
(defun path-has-a-loop-p (path start)
672770
(and (< 1 (length path))
673771
(loop :for (table compiler) :on path :by #'cddr
@@ -692,9 +790,16 @@ N.B.: The word \"shortest\" here is a bit fuzzy. In practice it typically means
692790
qubit-count))
693791
(generic-cost (occurrence-table-metric-fidelity
694792
(evaluate-occurrence-table (first generic-path) target-gateset))))
695-
;; it may be that non-generic gates have shorter routes to the target gate set.
696-
;; each possible such route begins with a specialized compiler.
697-
;; so, iterate over specialized compilers and see if they lead anywhere nice.
793+
;; it may be that non-generic gates have shorter routes to the
794+
;; target gate set. each possible such route begins with a
795+
;; specialized compiler.
796+
;;
797+
;; it's also possible a specialized compiler actually allows a
798+
;; broader set of gates with symbolic arguments, even if it's
799+
;; higher cost.
800+
;;
801+
;; so, iterate over specialized compilers and see if they lead
802+
;; anywhere nice.
698803
(let ((compiler-hash (make-hash-table)))
699804
(dolist (compiler generic-path)
700805
(when (typep compiler 'compiler)
@@ -708,10 +813,14 @@ N.B.: The word \"shortest\" here is a bit fuzzy. In practice it typically means
708813
(special-cost (occurrence-table-metric-fidelity
709814
(evaluate-occurrence-table (first special-path) target-gateset))))
710815
;; did we in fact beat out the generic machinery?
711-
(if (and (not (path-has-a-loop-p special-path (compiler-bindings compiler)))
712-
(>= special-cost generic-cost))
713-
;; then store it!
714-
(setf (gethash compiler compiler-hash) special-cost))))
816+
;;
817+
;; or does this special compiler actually allow a broader
818+
;; set of inputs than the generic ones?
819+
(when (or (compiler-allows-symbolic-parameters-p compiler)
820+
(and (not (path-has-a-loop-p special-path (compiler-bindings compiler)))
821+
(>= special-cost generic-cost)))
822+
;; then store it ya dingus!
823+
(setf (gethash compiler compiler-hash) special-cost))))
715824

716825
;; these are basically all the compilers we care to use; now we need to
717826
;; sort them into preference order.

tests/compilation-tests.lisp

+39
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,42 @@ CNOT 4 8
277277

278278
(deftest test-free-rx-rz-strings-reduce ()
279279
(%test-reduction-with-chip 3 (%read-test-chipspec "1q-free-rx.qpu")))
280+
281+
(deftest test-symbolic-parameter-compiles-in-examples ()
282+
"There have been bugs where symbolic compilers aren't found because non-symbolic ones are lower cost. (Bug #667)"
283+
;; Check we can compute this fact right.
284+
(is (quil::compiler-allows-symbolic-parameters-p #'quil::RX-to-ZXZXZ))
285+
;; If this happens to change in the future... let me know!
286+
(is (not (quil::compiler-allows-symbolic-parameters-p #'quil::EULER-ZYZ-COMPILER)))
287+
;; Some full stack test cases that arose out of bug reports.
288+
(not-signals error
289+
(quil:compiler-hook (quil:parse-quil "
290+
DECLARE theta REAL
291+
RX(theta) 0")
292+
(quil::build-nq-linear-chip 2)))
293+
(not-signals error
294+
(quil:compiler-hook (quil:parse-quil "
295+
PRAGMA INITIAL_REWIRING \"NAIVE\"
296+
DECLARE ro BIT[5]
297+
DECLARE theta REAL[2]
298+
H 1
299+
H 2
300+
H 3
301+
H 4
302+
H 5
303+
CPHASE(theta[0]) 1 2
304+
CPHASE(theta[0]) 2 3
305+
CPHASE(theta[0]) 3 4
306+
CPHASE(theta[0]) 4 5
307+
RX(theta[1]) 1
308+
RX(theta[1]) 2
309+
RX(theta[1]) 3
310+
RX(theta[1]) 4
311+
RX(theta[1]) 5
312+
MEASURE 1 ro[0]
313+
MEASURE 2 ro[1]
314+
MEASURE 3 ro[2]
315+
MEASURE 4 ro[3]
316+
MEASURE 5 ro[4]
317+
")
318+
(quil::build-nq-linear-chip 6))))

0 commit comments

Comments
 (0)