概念笔记 量子随机存储器

如何通过地址 10 找到并读出数据

假设面前有四个存储位置,每个位置都放着一个 0 或 1。现在,我们想知道地址 10 里存了什么。就从这个小问题出发:先找到位置,再把数据读出来,最后试着让地址处于叠加态,看看查询会有什么变化。沿着同一个例子往下走,就能逐步理解量子随机存储器的查询过程。

这四个存储位是我们为讲解准备的教学假设。走到代码部分时,会用 PivotQ 构造电路,再用 Qiskit 在本地 CPU 上进行理想态矢量模拟。PivotQ 目前没有可运行的量子随机存储器任务或真机读写数据,因此本页先陪你理解理想查询的规则。

普通内存和量子地址

先从熟悉的查表说起。程序给出一个地址,内存就返回那个位置保存的数据。比如读取数组里的 memory[2],我们要找的是从 0 开始编号的第三个位置。这个位置用二进制写出来,就是后面一直会遇到的 10。

“随机访问”这个名字容易让人以为地址是随机挑的。这里的意思其实是:你可以指定任意一个地址来访问。至于地址 10 里存了什么,还得查看内容;地址负责告诉我们去哪一格,内容才是我们要取回的值。为了把这两件事看清楚,这次每格只放一位数据。

接着,把同样的问题带到量子电路里。我们用两个量子位保存地址,再用第三个量子位接收查询结果,这两部分分别叫地址寄存器和数据寄存器。看到 |10⟩ 时,可以先读作“两个地址位分别处于 1 和 0 的状态”;写在它后面的 |0⟩,则表示数据位从 0 开始。

有了量子位,地址还可以处于多个地址态的叠加。我们希望查询后,每个地址分支都与它对应的数据关联起来,并保留分支之间的相对相位。这就是这里所说的相干查询。保留这些关系,后续电路才能继续改变振幅和相位,让不同分支发生干涉,也就是概率幅相加或抵消。

如果先测量地址再去查表,我们就只会沿着测到的那个分支继续,原来叠加态中的相位关系也无法保留。经典程序依次查询 01 和 10、把两个结果列在一起,同样不会形成这样的量子叠加态。先记住这个区别就好:查询要保留量子态里的关系,而最后一次测量能读出多少信息,我们会在文末回到同一个例子来看。

从两位地址开始

先把四个位置排好,依次标上 00、01、10、11。两位地址恰好能区分它们,因为每一位都有 0、1 两种取值,合起来就是 2² = 4 种;推广到 n 位,就能为 2ⁿ 个位置编号。

这次选中的是 10。读的时候,先看左边的 1,再看右边的 0。它表示二进制的 10,也就是十进制的 2。于是,我们已经知道要找哪一个位置,接下来只差一条到达它的路线。

  • 00
  • 01
  • 本例10
  • 11

按顺序读出路径

把这些位置画成一棵两层分叉的树,地址就可以变成行走路线。我们先约定:遇到 0 向左,遇到 1 向右。从根节点出发,按顺序读完 10,便会先向右,再向左。下面这小段 Python 把刚才的过程写了下来,可以先对照输出读一遍。

路径演算Python
address = "10"
turn = {"0": "左", "1": "右"}
path = []
for bit in address:
    path.append(turn[bit])

print(" → ".join(path))
print(f"存储单元 {address}")
预期输出地址 10
右 → 左
存储单元 10

看到“存储单元 10”,说明我们已经找对位置了。现在还没有读取内容,也没有改变数据量子位;这个位置究竟保存 0 还是 1,等拿到后面的存储表,就能继续往下算。

沿树形结构找到位置 10

  1. 1
    第一位:1

    从最上方的根节点出发,先沿右侧分支走到下一层。

  2. 2
    第二位:0

    到了右侧节点,再沿它的左侧分支走,就到达位置 10。

|10⟩00011011
顺着蓝线走:先右后左,最后停在编号 10 的存储单元。

回头看这张图,第一位决定第一层怎么走,第二位决定第二层怎么走,两层分叉就能到达四个存储单元。蓝线画出了确定地址 |10⟩ 的路线。

稍后遇到叠加地址时,这幅图仍能帮我们辨认每个地址对应的位置。不过,那时要同时考虑各个量子分支,并保留它们之间的相位关系;随机挑一条蓝线走,无法表达这样的查询。

在四个位置各存入一位数据

位置找到了,现在给这四格放上具体的内容。我们约定它们分别保存下面这些 0 或 1,作为这次讲解的存储表。先留意两格就够了:刚刚找到的地址 10 保存 1,稍后会一起用到的地址 01 保存 0。

教学假设:四个地址各保存一位数据
地址存储位
001
010
101
111

先读固定地址 10

先做一次最容易跟上的查询:把地址准备为 |10⟩,把接收结果的数据位准备为 |0⟩。表里告诉我们,地址 10 存的是 1。查询结束后,左边的地址保留下来,右边的数据位变成 1,整个过程可以这样读:

输入|10⟩|0⟩输出|10⟩|1⟩

如果想把这一步写成适用于任意地址的规则,我们用 a 表示地址,b 表示数据位原来的值,mₐ 表示该地址存储的位。下面的 ⊕ 是异或:存储位为 0 时保持数据位,存储位为 1 时把数据位翻转。

Uₘ: |a⟩|b⟩ → |a⟩|b ⊕ mₐ⟩

这里多写一个异或,是为了让查询可以撤销。量子电路中的相干查询必须可逆,而直接用存储值覆盖数据位,会丢掉它原来的值。仍以地址 10 为例:数据位从 0 开始,就翻成 1;从 1 开始,就翻回 0。因此 |10⟩|1⟩ 会变成 |10⟩|0⟩。对同一地址再查一次,数据位便回到原值,你可以用这个办法检查自己是否读懂了规则。

再读两个地址的叠加

固定地址的过程清楚了,就可以再向前走一步。让 01 和 10 组成等振幅叠加态,数据位仍从 0 开始。存储表没有变,查询规则也没有变:01 这一支查得 0,10 这一支查得 1。量子变换具有线性性质,因此对叠加态做同一次理想查询,就得到这两个分支各自变换后的叠加。

读下面的式子时,可以先只看两个分支各自发生了什么。√2 用来归一化,让两个分支的测量概率各为 50%;⊗ 则表示把地址和数据位组合在一起。有了这些记号,就能把刚才的叙述完整写下来:

输入(|01⟩ + |10⟩) / √2 ⊗ |0⟩
输出(|01⟩|0⟩ + |10⟩|1⟩) / √2

再看一遍输出:地址 01 和数据 0 在同一支,地址 10 和数据 1 在另一支。加号把两个量子分支写成叠加,地址和数据的数值本身没有相加。

现在,地址与数据位形成了纠缠:它们的状态需要一起描述,无法拆成两个彼此独立的状态。查询把这种对应关系留在了量子态中,后续电路可以在测量前继续处理它。这个小例子就先走到这里,接下来用程序看看刚刚推导出的关联态。

一次测量会得到什么

手上的存储表和查询规则已经足够写出一个小程序了。我们用 PivotQ 构造可逆查询电路;它的电路接口直接复用 Qiskit 对象,因此接着就能交给 Qiskit Statevector,计算查询后的理想量子态。

对照代码前,先认清三个量子位:q2、q1 负责地址,q0 负责数据。Qiskit 按 q2q1q0 写出的三位标签,会被程序拆成 |地址⟩|数据⟩ 的样子,这样就能和前面的公式一一对应。四个函数也沿着同一条思路展开:准备输入、执行查询,再查看状态。

  1. 先看 fixed_case():它用 X(2) 把地址准备为 |10⟩,数据位保持 |0⟩,正好对应第一种输入。
  2. 再看 superposition_case():从地址 |01⟩ 出发,H(2) 产生 01 和 11 两个等振幅分支,随后 CX(2, 1) 把其中的 11 变成 10。这样就准备好了第二种输入。
  3. 两种输入都交给 append_query()。它照着存储表,只在值为 1 的地址上用 CCX 翻转数据位。遇到地址中的 0,会临时对相应地址位加 X 来匹配控制条件,查询后再撤销这些 X。
  4. 最后,show_components() 用 Statevector 算出非零基态的振幅,并把振幅模的平方打印为理想测量概率。到这里,我们看到的是计算出的状态与概率,程序没有进行随机抽样。

为了把例子完整跑通,我们把四个存储位直接编进了门电路。这样可以演示可逆查询规则;可动态读写的量子随机存储器器件还需要更多实现,这段代码没有创建这样的器件。

如果想亲手试一遍,可以使用 Python 3.12,在 PivotQ 仓库根目录运行 python -m pip install ./packages/framework,也可以直接使用仓库已配置的统一环境。接着把下方代码复制并保存为 qram_query.py,在同一环境中运行 python qram_query.py,然后对照下面的输出,看看它是否和刚才的推导一致。

代码 2 · 理想查询
#!/usr/bin/env python3
"""用 PivotQ 构造电路,以 Qiskit 理想态矢量重现页面中的四位置查询。

四个存储位是教学假设;此脚本不提交 PivotQ 任务,也不访问真实 QRAM。
"""

from __future__ import annotations

from pivotq import QuantumCircuit
from qiskit.quantum_info import Statevector


# Qiskit 的三位基态标签按 q2 q1 q0 排列;前两位是地址,末位是数据。
MEMORY = {"00": 1, "01": 0, "10": 1, "11": 1}


def append_query(circuit: QuantumCircuit) -> None:
    """在三量子位电路上追加 |a>|b> -> |a>|b XOR MEMORY[a]> 查询。

    q2、q1 是地址位,q0 是数据位。存储位为 1 时,用受控 X 翻转 q0;
    对地址中的 0 临时加 X,查询后撤销,保留地址和分支间的相位关系。
    """
    for address, stored_bit in MEMORY.items():
        if stored_bit == 0:
            continue
        zero_controls = [wire for bit, wire in zip(address, (2, 1)) if bit == "0"]
        for wire in zero_controls:
            circuit.x(wire)
        circuit.ccx(2, 1, 0)
        for wire in reversed(zero_controls):
            circuit.x(wire)


def fixed_case() -> QuantumCircuit:
    """准备 |10>|0>,追加查询后返回电路。"""
    circuit = QuantumCircuit(3)
    circuit.x(2)
    append_query(circuit)
    return circuit


def superposition_case() -> QuantumCircuit:
    """准备 (|01> + |10>)|0>/sqrt(2),再追加同一查询。"""
    circuit = QuantumCircuit(3)
    circuit.x(1)
    circuit.h(2)
    circuit.cx(2, 1)
    append_query(circuit)
    return circuit


def show_components(circuit: QuantumCircuit) -> None:
    """打印非零基态振幅及其理想测量概率。"""
    state = Statevector.from_instruction(circuit)
    for basis, amplitude in enumerate(state.data):
        if abs(amplitude) < 1e-10:
            continue
        if abs(amplitude.imag) > 1e-10:
            raise ValueError("本教程中的计算预期只有实数振幅")
        label = f"{basis:03b}"
        print(
            f"|{label[:2]}⟩|{label[2]}⟩  "
            f"振幅 {amplitude.real:+.6f}  概率 {abs(amplitude) ** 2:.0%}"
        )


if __name__ == "__main__":
    print("固定地址 |10⟩|0⟩:")
    show_components(fixed_case())
    print("叠加地址 (|01⟩ + |10⟩)|0⟩ / √2:")
    show_components(superposition_case())
运行输出CPU 理想态矢量
固定地址 |10⟩|0⟩:
|10⟩|1⟩  振幅 +1.000000  概率 100%
叠加地址 (|01⟩ + |10⟩)|0⟩ / √2:
|01⟩|0⟩  振幅 +0.707107  概率 50%
|10⟩|1⟩  振幅 +0.707107  概率 50%

先读固定地址那一行:|10⟩|1⟩ 的概率是 100%,正好对应地址 10 保存的 1。再读叠加地址的两行:|01⟩|0⟩ 与 |10⟩|1⟩ 各自的振幅约为 0.707107。取振幅模的平方,就得到各 50% 的概率。

这时很自然会想到开头留下的问题:既然屏幕上有两行,一次测量能把两行都读出来吗?Statevector 能列出理想态的所有非零分支,是因为我们正在查看模拟器计算出的量子态;这里没有执行随机测量。若直接测量地址位和数据位,单次只会得到其中一组。每次重新准备同样的输入、完成查询再测量,重复多次后,两组结果的出现频率才会趋近各 50%。

于是,从寻找地址 10 到查看叠加查询,我们走完了这个例子的全过程:定位、可逆地读取,再理解测量结果。若继续走向真实设备,还要准备存储数据、实现相干寻址,并处理噪声与读写成本。这些都超出了这张四格存储表的演示范围,因此这里的输入与输出还不能用来判断实际硬件性能或算法加速效果。