ZK Whiteboard Sessions - S3M2: High-Performance Engineering for SNARKs with Jim Posen

16 Sept 2025 · 1:01:09

Nico Mohnblatt is joined by Jim Posen, CTO at Irreducible, for a ZK Whiteboard Sessions module on high-performance engineering for SNARKs. Jim breaks down computational and memory bottlenecks, how parallelism and SIMD instructions speed up constraint evaluation and polynomial commitment openings, and where custom hardware like FPGAs and ASICs and proof recursion fit into scaling SNARK proving.

Speakers

Topics

Transcript

Welcome to this module of the ZK Whiteboard  Sessions. I am Nico, I'll be today's host, and today I'm super happy to have a  return guest, Jim from Irreducible. Hi, I'm Jim, I'm the CTO at Irreducible  and I'm very excited to be here. Great, what are we talking about today, Jim? Today we're going to talk about high  performance engineering for SNARKs and how we can make the best use of our technology  to implement these protocols very efficiently. So what does that entail? What are the  themes that we're going to go through? This is really a story about how we identify  bottlenecks, decide whether they're computational bottlenecks or memory bottlenecks, and break  through them by using software engineering, hardware engineering techniques, and we're  really going to pursue the story of how do

we exploit parallelism to distribute our  work over multiple resources to bring down the time it takes to produce a proof. Cool,  shall we get started? Let's get started. We need to recall a basic SNARK protocol,  so in this session I'm going to focus on hash-based SNARKs, if that's okay, or  hash-based SNARKs, for example, STARKs, and we're going to have a little toy example of  a STARK over the Baby Bear field. We, I think, spoke in our last module about small field STARKs,  so the Baby Bear field is a 32-bit prime number, and it's become very popular for STARKs  because it's compatible with the NTT,

and we'll have a STARK protocol where we have  a table that has three columns, and we want to evaluate a constraint over it, for example, a  times b minus c equals zero, and we want this to be true at all rows in our trace, and if we  recall the major steps that we're going to do in a STARK are number one: witness generation,  so this is when we arithmetize the problem and fill in the trace matrix. Number two: we need to  commit the trace and in a hash-based SNARK this is going to involve a number theoretic transform  for error correcting code and also a Merkle tree.

The third step is constraint evaluation. This is  when we take this expression and evaluate it over every row, and the number, the last step is the  PCS opening, and we'll be a little non-specific about the PCS. We can imagine this being either  Fry or something like Ligero, and as a rough sense of how costly these different operations are, if  you run Plonky3 for a Poseidon, every different constraint system might have a different profile,  but we could roughly think about this as being

10 percent of the overall time, this as being 30  percent, 30 percent, 10 percent, and 20 percent. Why is this? Number theoretic  transforms are asymptotically more expensive. They're super linear,  whereas these steps are both linear. Hashing is just a sort of different type  of operation, and it's more expensive, and our PCS opening is going to be more  expensive than constraint evaluation, because we're doing extension field operations  instead of small field operations. Makes sense.

Just to jog my memory, entity here is because  we're doing this encoding of whatever is there, and then constraint eval is when we  check everything is indeed zero. Exactly. We're also actually using the NTT for the  constraint evaluation because this is a polynomial multiplication that has  to happen over an extended domain. Makes sense. How do we go about making a STARK that's doing  this operation faster? Well, first thing we want to do is just minimise the number of instruction  cycles that it's going to take to do this. What we'll see is when you look at all of this, there's  really three operations that we care about.

Base field multiplications of Baby Bear  elements, extension field multiplications of Baby Bear 128-bit extension elements, and  the hashing, which is a sort of special thing. You'll remember that even though the entries in  our trace are 32-bit elements, we need to use Baby Bear degree four extension elements, which  are basically vectors of four Baby Bear elements, and there's a multiplication  algorithm associated with them.

Here, viewers can refer back  to your previous module, right? Exactly. In order to just reduce the number  of instruction cycles in a CPU implementation, we'll use standard techniques like turning  on compiler optimisations, inlining, looking at assembly, but we'll quickly find that we want  to increase our field multiplication throughput, and there's not really a way to do that by  reducing the number of instruction cycles. So how are we going to increase multiplication  throughput without reducing the number of cycles? We need to exploit parallelism.  So one instruction is going to do more multiplications than it did before? Exactly.

So different CPU architectures have different  instructions, but this is called SIMD parallelism. What does that stand for? SIMD is single instruction multiple data, so one instruction is going to operate  on a 128-bit register. We'll think about Arm NEON as being giving 128-bit operations,  and your 128-bit register is going to get split into four lanes. Each one is a  separate Baby Bear element, and so on. Now we'll do the same number of instruction cycles  to, say, multiply Baby Bear elements. So if we

have two of these registers, we can multiply  them, but every time we do that sequence of instructions, it's going to multiply all four of  these in parallel. These would be called lanes. So we're going to use these SIMD  instructions to exploit parallelism, and where does this help us in our STARK? Yeah, so I guess we said entity  was some multiplications. Yeah, exactly. There's field multiplications  happening really in all the steps except for hashing. We'll get back to the NTT because this is a bit of a trickier case. So let's  start with constraint evaluations.

So what does this mean again? It means we  have many rows. We could think about having several million rows or so, and we want  to evaluate this operation on each row. This looks like what we'll call an embarrassingly  parallel operation. You don't need to do any work to find the parallelism. It's just you're  doing a separate thing for each row. So how would we lay out a SIMD operation for this? We're going to treat our columns as  vectors of SIMD registers, so we might put four elements here sort of column-wise.  So into one of these, this will be 128 bits.

We'll do that for A, B, and C.  And then if we had a million rows, now we need to do a million divided  by four multiplications on these SIMD registers. So this is going to reduce the  total number of cycles we need to compute the operation over the whole matrix  because we're exploiting parallelism. There are some interesting considerations  that happen when you have to engineer one of these systems. One is that these registers  are more efficient to use if they're stored at memory addresses that are aligned with the  size of the register, meaning in the programme, the memory pointer to the beginning of A should  have its bottom. It should be a multiple of 128.

It should have its bottom seven bits  cleared, which is not something that a normal memory allocator will do for  you unless you pay specific attention to this. So alignment is one consideration.  Another interesting engineering challenge is that different CPU architectures have  different SIMD instructions, so we'll normally need to hide these behind compiler  feature flags and specifically manage that, but that's all going to be handled in the  abstraction of our field library generally.

Or have a library that's specific  to one specific processor, right? Yeah exactly. Okay, I see. So where else can we apply SIMD  optimisations? We said constraint evaluation is an easy place. Our PCS  opening is another interesting case where we have parallel operations, but  they're over extension field elements. So what does this mean? I was showing up  here that we have these Baby Bear quartic extension elements, so I'm going  to sort of write this as A,B,C,D, So these are the four Baby Bear elements that make  up the limbs of one quartic extension element.

So how do you think we should  represent this in memory? I mean, can we plug them directly into our lanes? Yeah, that seems like a very natural thing to do. We have these 128-bit registers.  We can put one limb per lane. Actually, that's going to generally harm  our ability to do parallel operations. If we were to do this, we need to take a bunch  of cross terms when we multiply two elements, and that's going to require us to use SIMD  shuffle instructions. This is possible, but it's just adding more cycles for something  where we want to maximise our throughput.

Just to be sure, this is because...  So addition would be limb by limb, but multiplication has cross-limb  things happening, right? Exactly. If I recall correctly, it's something like  17 different cross products of limbs in order to do a Baby Bear quartic extension  multiplication with reductions. So instead, we could actually think about using four SIMD  registers to represent many different Baby Bear extension elements. So think about having four  quartic extension elements and laying them out where all of the A limbs from one live in one SIMD  register, all the B limbs from this small element

live in another SIMD register, here's another  SIMD register, and here's another SIMD register. When we want to multiply two of these elements  together, it's easy to take the cross terms because we can take this register times... If  we wanted to multiply this by E, F, G, and H, it's sort of easy to take the cross term of C  times F and C times G, and we're just doing four extension field multiplications at a time. So we  are actually able to, even though our PCS opening phase is doing extension field multiplications,  if we choose our memory representation in a clever

way, we can get sort of embarrassingly  parallel speed up as well using SIMD. Okay. Is there an issue where here we've aligned  our memory in a certain way to fit, you know, ABC here, and do we need  different alignments to be working on extension fields? That's a great question. The alignment doesn't need to change because  we care about alignments for loading data from memory into a register, and that just requires  the alignment of the register size. But it's a common abstraction to have packed field elements  or objects that represent a fixed length vector of field elements. And in this case, if we  had the amount of data that it takes to store

a packed extension field element is equal to  the number of lanes times the extension field. So it's bigger than the amount of data to store  like one packed Baby Bear element, for example. We can go further with SIMD parallelism. So it actually makes sense to often  implement SIMD hash functions. If we're building a Merkle tree, then you are  performing many separate hash operations in parallel. So you could use SIMD lanes to store,  you know, different inputs to the hash every time.

Okay. Yeah. Different words in your hash, basically  one SIMD register, each lane is performing a different hash function at the same time.  And then we need to talk about the NTT. So let me erase this part. So how does the  NTT algorithm work? You basically have a, let's say a vector of elements that you want to  NTT, this will be something like 256 elements.

And we generally write this or we can visualise  this as this butterfly network. I don't know if we've talked about this before, but you're  going to to begin with every pair of adjacent elements and do an operation where something  like this, and Y is going to be computed as a linear combination of X and W, and Z is  going to be a linear combination of X and W. The memory layout of this is something  like this, where we take pairs of adjacent elements. And then on the next layer, we're  going to take kind of larger strides. I see. And then on the last layer, we're  going to take larger strides than that.

So we can parallelise through each layer, but  that isn't the easiest parallelism to exploit. Okay. So there's another way of looking at the  NTT, which makes the parallelism, I think, more apparent, which is if we have this vector,  we're going to first reshape this into a matrix. Let's call it 16 elements by 16  elements. There's an algorithm called Bailey's NTT or the four-step NTT, which  is we are going to take our matrix.

The first thing we're going to do is perform,  well, let's say we're going to transpose it. We'll do NTTs independently on each row. And so now we have 16 parallel NTTs  that we can do across each row. We have to transpose it back, and then  we will do, we'll complete it by doing 16 row-wise NTTs again. They're actually NTTs  onto different cosets in the second phase.

But it's to hold that we get to work row-wise. We get to work row-wise. We get to  introduce parallelism by reshaping into a matrix at the cost of having to do these  two transpositions. So these become the only sort of bottleneck in the NTT where we don't get  to parallelise as much as we might want to. So at this point, we've basically talked  about everything except witness generation, which we'll hold off on, and shown how there  exists parallelism in all of these operations that we can take advantage of. And the first way  we're going to do that is with SIMD instructions.

So how do you think we're going to go even  faster than just using these SIMD operations? Right. So at this point, we have one processor.  We're using as few instructions as we can. And we're making sure that each  instruction has as much data as I can treat. Exactly. So do we have more processors? We do. Modern CPUs have many different  cores, and you programme them using many execution threads that will get scheduled onto  different cores. So let me clear some space. So a modern, say, quad-core processor  means the CPU... I guess it's in the name, right? Has exactly four cores, right?

So this is our CPU and this is our CPU. And we're going to flesh out this diagram in a  little bit. But we have these different processing units. We've already shown that the algorithms we  want to do have parallelism that we can exploit. So if we took our big table,  we can just split it into, say, four segments and do the polynomial evaluation  for the first quarter of it on one CPU, on one core, the next core would handle the  second one, the next segment, and so on.

Do we then need to do some work  to put everything back together? Well, the awesome thing is they're all  going to have shared access to main memory. So here's our memory. And on a chip, you  have the main memory, which is like DDR, but then every core has a local cache. So you  may have heard about different cache types. The smallest and most local  one is called the L1 cache. Not the layer one blockchain.

Right. The L1 cache is the fastest to access.  So anytime a core needs to access memory, it will bring it into its local cache,  operate on it, and then write it back. So when we are operating on  multiple segments of the same table, you only really need to transfer each word in  the segment to the cache once if we're only using it once. So for something like constraint  evaluation, which is a sort of streaming pass, which is you're doing one pass over the  table, this has a really efficient memory access pattern. What about something like  the NTT where we had this transpose though?

So what does it mean to take a big block  of memory that's stored in main memory and then read different strides of it or different  columns of it into different CPUs? We're going to start seeing a memory bottleneck. So this  means that as we increase the number of cores, roughly all of the steps except for  the NTT should speed up pretty much proportionally to the number of cores we  have until we hit a memory bottleneck. And the first place that's going to happen is the  transpose that's happening in our NTT or however we choose to do strided accesses. Where does  this come from and what can we do about it? Well, we have a memory hierarchy in a CPU, meaning  we have layers of caching, we have memory,

and then another thing that every CPU has is  called a translation look-aside buffer, the TLB. This is when you need to resolve an address  that you have in your programme to where it lives in physical memory. And this is  basically another type of cache that if you have a cache miss on any of these different  caches it's going to slow you down. So that's why you want all of the memory accesses  to be very sequential and predictable and an access pattern like the transpose  in an NTT is going to violate that.

We'll get back to what we can do. Well,  ultimately what can we do about memory bottlenecks? Well, we can get  better technology for memory. We can find memory that instead of using  GDDR6 we'll use GDDR7 or the new generation. So do we mean it's just faster to  transfer between a core to the memory? Or generally we'll be talking about higher  bandwidth memories. So we care somewhat less about the latency of accessing memory  than the bandwidth of loading a large segment of main memory into a cache. Putting  aside the question of memory bottlenecks, if we're still compute-bound, what can we do?

If we want to lower the, you know, we  can't scale up the number of CPU cores infinitely without paying a lot more  for it. So how can we get more compute? So we can't have more cores  per CPU, we can have more CPUs? Yes, exactly. You can have more  CPUs and, well, actually we can have more cores but we need to move to a  different type of hardware which is a GPU. Okay. So now we're going to go from running operations  on a CPU to a GPU. This stands for graphics processing unit and but they've really come a  long way since the days of being only used for

graphics. You probably know that they're being  used widely in AI because and what do they do? Well they're a different processor type that  just has a lot of compute for parallel operations and they're less flexible but if you really  need high throughput of low level arithmetic operations like integer multiplications  and floating point multiplications a GPU is specially designed for that.  So to give you some rough numbers, if you look at cloud available compute  resources on AWS you could get an ARM CPU instance which would be something like a a  C8g ARM instance with 16 cores for roughly the same price as a G6 instance on AWS which has an  NVIDIA L4 and this thing has 6,144 CUDA cores.

Wow. So can we think of those CUDA cores as  almost equivalent to like a CPU core? Each CUDA core is going to be less powerful  and more limited in its functionality but we have so many more of them that the overall  throughput of arithmetic operations like integer multiplication is going to be  a lot higher for the same price point. So if we were to draw like something  similar to this diagram for the GPU? As I recall for an L4 you have what they called  many different SMs, I think those are Simultaneous Multiprocessors, so you have something like  48 SMs in a grid and each one has many cores.

So this is what our GPU will look like and  in many other regards it actually looks a lot like a CPU. You have a global memory, this  might have a different underlying technology for accessing global memory but you have a  global memory and each SM has a local memory which can be used either as a cache or in a  way that you have more manual control over. So what are some of the other, basically  is this a free speed up? Yes. Not really, we actually need to put in quite a lot of work  to figure out how to use this device efficiently.

So the first thing is it's not as  flexible as a CPU instruction set, you can't just write rust code and compile it and  run it on a GPU without doing something special and so we need a library that's going to  help us access the GPU resources for every hardware vendor is going to have some.  They have their own libraries? They're going to have their own library, so  most people in the space are using CUDA which is an NVIDIA specific compiler and tool  chain technology for programming NVIDIA hardware.

This allows you to write C++ code but you need  to think more carefully about how it's going to map onto the GPU hardware and what do we need to  do to run this from our rust code? Ultimately we need to write the C++ code and link it to  our rust library. Without going more into the tool chain aspects, the biggest conceptual  difference is that we can't deal with memory in the device in the same way that we handled  regular memory pointers to our system memory. So as I said you have a main memory in the  GPU and in our programme we need to keep track of the fact that our table that we're  actually trying to operate over does not live

in a shared memory space between the GPU and  the CPU. First we need to transfer it from our motherboard. We'll call this the host and this  would be connected over a system bus to the GPU. These are two separate pieces of hardware, two  separate chips generally. We need to actually transfer data from the main memory here to  the GPU and our software again needs to track where in the GPU's memory space each piece of  content lives and that's more code to handle.

Is this something for the software to track or the engineer to keep in mind and write  the software with that view in mind? Absolutely both. You as an engineer need to  be pretty thoughtful about where memory is going to live and one thing that could help you  manage that complexity is a hardware abstraction layer. So this is something that  a high performance cryptography library might have to introduce  themselves which is a software abstraction for running operations  over different types of hardware.

I see. Oh and I guess this also  allows to get away with you were saying each vendor will have  its own language like CUDA or yes so if you also wanted to support Apple GPUs  and like the Metal framework you could write a hardware abstraction layer that makes it so that  you as a cryptography engineer wouldn't need to know the details of these libraries. So what is  like how do we make the most efficient use of a GPU? There's a like a principle in GPU programming  that what you want to do is feed the beast.

You have all these really powerful processing  cores and the worst thing that could happen is that they're sitting around idling, waiting  for a memory transfer to happen before they can start their work. So if we have a paradigm  in which we need to transfer data from the CPU to these multi-processing cores over a  bus which is a fairly low bandwidth link, we want to make sure that either we can  generate the data in the device to begin with that we actually can avoid the memory  transfer altogether or that while we're transferring memory we're also working on  the data that we received last. So if we have like a gigabyte of memory that we  want to process in a streaming fashion

so we have a gigabyte of data here and we want  to you know send it to the GPU and get it back. But our bus doesn't have like one  gigabyte sort of bandwidth I guess. Yeah or like instantaneous. Yeah  there's some latency to doing this and even if we had you know one gigabyte  per second speed that would take a full second to process the data. So the most  naive thing we could do is send one gig, wait for that transfer to complete, do all  the processing, and then transfer it back.

And if you know let's just say we can we  can have a bandwidth of one gigabyte per second then it's one second to transfer it, our  processing time, and then one gigabyte back. Definitely not feeding the beast here. Definitely not. It's what are our processors doing  while we're transferring? Like absolutely nothing. So this is the concept of operation  pipelining. We want to pipeline our memory transfers so that it's overlapping  with processing. So as soon as we've, say, transferred the first megabyte, so we  can think about this as being like time. So let's call this host to device transfer.

So if this is our host to device transfer and  it's going to be you know this amount of time, and then on the bottom we  want to do device to host, and then in the middle we want to we want  to compute. What we'd like to do is as soon as we've transferred like the first  megabyte we can begin computing on this. And then as soon as we've computed the  first megabyte we can send it back. We can immediately start sending it back assuming  our link is bidirectional and we can send data in both directions. So what this lets us do  is for essentially the amount of time it just

takes to transfer the data as assuming that  data transfer is in fact our bottleneck here, we don't need to wait for the compute  to happen in the middle and we can overlap the transfer on the way  to the device and the way back. And in fact even we can take this this further  if our computation involves multiple steps it might make sense to partition the  processors here so that like the first half of the processors are doing the first  phase of it, they transfer data to memory, and then the second like half of  our processing cores can pick up.

So this can also be pipelined essentially? This can be pipelined. Where might that come into  play? Well for commitment we need to do these number theoretic transforms and then and then  merkle hashing and if we structure things in the right way we could potentially begin our merkle  hashing while we're still doing part of the... As soon as part of this becomes  available, hash it. Exactly. Nice. So one question that comes to mind for me  is could we have been doing this pipelining without even going to GPUs, right? Could we have  been doing this in the first place with our CPUs? Yeah that's a really interesting  question. We need to think about what pipelining is is giving us.  So what pipelining lets us do is put our compute resources to work if they  would otherwise be blocked on something.

So I gave the sort of example here of pipelining  data transfers with compute because actually the bandwidth on this bus link is is a different  resource from our compute resources. If we have algorithms like this and this which are  embarrassingly parallel, they're not really getting blocked, they're just compute bound, then  we can put all of our cores to work and it's not going to help us too much. But in other settings  we can be thinking about pipelining if we're doing signal processing streaming applications or if  we're blocked on disc IO or anything like that.

I see, or blocked on waiting for one step  to finish to the next, right? Exactly, yeah. Actually with that in mind, are there  specific SNARKs that have a structure which allow pipelining from one step to the next or are there  SNARKs that are really bad at this, for example? So a really interesting resource in SNARK  design is interaction. This idea of taking an interactive protocol where the verifier  sampling randomness allows them to avoid more work. So a STARK is an interactive protocol but  there's very few rounds of interaction actually. We need to wait for the commitment before we  proceed to this phase and then we might have some interaction rounds in our PCS opening.  But there are other SNARK designs like some check-based protocols or protocols based on  GKR that may have overall less computation

but use more interaction. And what does that  look like for a high compute environment? The GPU multiprocessors might be waiting for the  last round of interaction to complete, for the verifier to sample new randomness to get started  again. So there's an interesting design space of understanding how compute-bound our problem is.  And if it is, we might introduce more interaction. But if that's not a problem, then we might reduce  interaction in order to feed the beast. I see.

And so the reason we have to wait for  one round of interaction to finish before doing the next is because we usually  do Fiat-Shamir and we need to commit or hash everything that's happened in one step to get  the verifier challenge, right? Exactly. Okay. And so concretely you were giving examples like  something that uses a sum check protocol will use a lot more interaction than what we have here,  which is just interaction I think here, right? Yes. Okay. So here we showed a new  level of a new degree of parallelism. We took a new hardware platform that gives us  many different cores that we can operate over. And so what if we want to reduce the time  to produce a proof even beyond that? Well, we're going to scale up to multiple machines.

Right. We're going to start talking about  this whole setup multiple times. Just you could think about a rack in a  data centre with many servers, each having many GPUs connected  to different PCIe links. Okay. So how do we manage that? I mean, now we're talking about SNARK  proving over a cluster of servers. How do we feed the beast? So we have a new resource  limitation in this setting, which is network bandwidth. Okay. If  we have many servers and they're...

So this is the link between the  servers? This is... Yeah, exactly. The network links connecting  multiple servers to each other. And this, for example, becomes a big bobbleneck  in AI training where you have really advanced technology for high bandwidth data transfers  within a data centre so that you can distribute your training over a large cluster, but they're  accessing a single large data set. In the SNARK design space, we have a really interesting way  of getting around the bandwidth limitations.

So maybe I can draw a little, like a rack  of servers here or something. So we have server one and two, and these are actually just  different boxes and each one has a CPU. And a GPU. And a GPU. And its own memory. Yeah, exactly. And they're connected  generally with like a top of rack switch, but they'll be connected with Ethernet  or something like this. And so what can we do to avoid any sort of bandwidth bottlenecks?

Well, there's an amazing thing about  the SNARK setting, which is SNARKs essentially compress verification, right?  That's, in fact, the entire point of a succinct proof that you want to take some  statement and compress it into something smaller. So we can exploit a sort of compute  bandwidth trade-off by leveraging recursion. So if we want to compute a virtual machine  computation for if we're trying to prove, you know, 1 billion RISC-V cycles.

So can we think of this as our computation  trace with 1 billion rows, give or take? Yeah, you could think about this as  a computation trace with 1 billion rows. The first thing to note  is the total size of that trace, if it was all put into some memory at the same  amount of time, at the same point in time, would be enormous. Probably hundreds  of gigabytes or potentially more. So actually, total amounts of memory might  become a limitation for us as well. But using recursive techniques, what we could do is split  this into, let's call it a thousand segments, each 1 million cycles. And instead of  proving all 1 billion cycles at once,

we'll first generate independent proofs  for each segment of 1 million cycles. And then we will recursively  aggregate them. So if the, let's say the working memory size to produce  a proof for 1 million cycles is 16 gigabytes. As long as each machine has 16  gigabytes, then we can do this, I guess. Yes. Okay. As long as each machine has 16 gigabytes  and we're going to compress, the output of the proof might be 200 KB or something  like this. The next thing we need to know is how costly is it to do a recursive proof  that compresses two leaf proofs into one?

And we want to do this because, so we  have a thousand proofs for like one proof per segment. Yes. And we want to bring  it down to just one proof for the whole thing. That's exactly right. We want to compress an arbitrarily large  computation for any number of cycles into a single proof that's a manageable  size. And we're going to do this by distributing the segments of  computation over the entire cluster. And then distributing the work of  recursively compressing two proofs into one. I see.

So, you know, we can think about these being our  billion cycles and we're actually going to build, it's pretty common to just build a  binary tree of proofs where this proof is going to produce a new proof  blob here. And then at the end, as you said, we're going to take all of  these proofs and create our final proof. Okay.

And why are we doing this as a tree? The reason we're doing this, so  that's a great question. What else could we do? We could potentially  just go through this in sequence. That would be in literature, sort of like  IVC, incrementally verifiable computation, where you're updating a running proof.  And the reason we want to do this in a tree fashion is because that allows us to  have more parallelism. Always the answer. Always the answer. So how deep is this? Well, if we have  a binary tree, it's log depth in the number of segments. So it's actually, you  know, log two of the number of segments.

And this essentially dictates  our latency of computation. And also if we think about it from a  resource utilisation point of view, having more proofs to produce at any given time  means we get to put all of our servers to work, which means we get to put all of our  GPU cores to work and so on. Right. So as we move up the tree, we can use, we can free  up some GPUs, some actually servers in this case. Yeah. I mean, if we're just trying to produce  a single final proof for 1 billion cycles, as we get closer to the end of the  tree, our servers are going to start idling, but they've done a lot of  work at that point already. Fair.

Another really interesting thing happens when we  get up to the level of cluster level computing, which is we're going to run into a new  bottleneck and that's around witness generation, which is pretty much the  only thing we haven't talked about as an explicit step. So we talked about how this  is parallelizable. This is parallelizable. We talked about at this point, four different ways  to compute when you have enough parallelism. When you get up to the level of essentially having as  many servers, many GPUs as you want, what is the sequential bottleneck? It's actually just the time  to emulate the RISC-V execution and to store all of the information needed about that execution to  do the parallel work of producing segment proofs.

So to fill out the trace? Or it's  more than that? I think to begin with, it's to fill out the trace, but we  can actually be more clever than that. We don't need to fill out the entire  trace sequentially. What we're going to do is a sequential preflight  execution. This is a term that RISC Zero uses and that I first learned from them. You'll do a preflight execution, which is a pass  over the computation that is meant to be as fast as possible, but it's going to log enough  data about what happens in the middle of the execution to then produce the segment witnesses  in parallel. So if we have a thousand segments and let's just say we snapshot the entire  memory at every segment of 1000, then we'll

do a second pass over the entire execution,  but in parallel, running over every segment, doing the more expensive work of filling out the  trace table. And the other thing this is going to let us do is save memory because we can use less  memory to store the snapshot artefacts that we need during preflight execution than the amount of  memory required to store the entire segment trace. So preflight means I go over my 1  billion cycles, I mark a few checkpoints, and then it allows me to fill up traces from  each checkpoint. That's right. Brilliant.

And we can in that way also begin to  parallelise our witness generation. So are there any teams using all  these techniques that we've seen so far and implementing everything in one go? Yeah, there are several really talented  techniques that have achieved basically the unthinkable benchmark of having  real-time proving for Ethereum blocks, meaning the ability to prove an Ethereum  block execution in lower than the Ethereum slot time. So Succinct and SP1 have been able to  do this. RISC Zero has been able to do this with their STARK technology as well, using exactly  this entire set of techniques of GPU programming, cluster-level programming, fast witness  generation with preflight execution,

and achieve something really remarkable  using many servers coordinated at once. So I guess I'll keep prompting. Can we  do better? Can we be faster or get the same speed with fewer resources, fewer servers? Yeah, so what are the resources we care  about at this point? We care about the latency of computation. I think if  we tried to make that 10x smaller, we would run into new bottlenecks  using this set of techniques. But also we start to care about the cost of  computing this as well. Money is a resource, and we might care about the power required  to produce a large SNARK computation.

So really, the future of this  industry is custom hardware. When it's a big enough problem in a big enough  market, we can reduce the cost of producing many proofs by having dedicated chips that are  optimised for these types of computational problems. So where in the story does this  fit in? This is essentially about reducing, going all the way back to the  beginning, reducing the number of instruction cycles required to do  the computation in the first place. So these field multiplications  that we have to do everywhere.

Yeah, being able to do field  multiplications with either fewer resources, or in fewer instruction cycles, or at a higher clock speed. And for that, we need  to design new hardware. So let me erase this. I guess this is analogous to how  GPUs first came to be, right? Exactly. I mean, if we think back  to early graphics programming, it was a software engineering marvel to  be able to produce stuff like Doom with regular CPUs. And then now we have rich  graphics by developing custom hardware with processing units that are  adapted to that specific problem.

So what does it take to actually do a field  operation in a CPU or a GPU? For a prime field, it looks like an integer multiplication with  additional cycles to perform a modular reduction. Right. So one thing we could do is just make  the native data type of our processing unit to be field element multiplications.  We could even handle extension field multiplications using instructions instead  of many cycles of different instructions. The other thing is when we look at NVIDIA  GPUs and what they're optimised for, they are more limited than CPUs, but they  still optimise for floating point arithmetic.

Mm-hmm. And in fact, they often advertise  the number of FLOPS, floating point operations per second that they're able  to do. They have flexible control flow. So this is like if statements? Yeah. And they have a very  flexible memory hierarchy, meaning your core can access any  piece of data from global memory, and the network that's routing data between  the cores needs to be able to handle these.

So talking specifically about GPU cores, right?  I'm talking about GPU cores. That's right. Just because the word core has been  used at every layer almost. Okay. Global memory with cache. So because  if we wanted to design hardware that's specifically good for SNARK use  cases, we can adapt them to the actual types of memory access patterns and  the actual type of computation. So we might be able to get away with a memory model  that's more limited than global memory.

We also see this happening in the AI space where  you have experimentation with different types of memory models like systolic arrays that can be  targeted for these specific types of workloads. We may not need flexible control flow because  we're basically doing streaming passes over data, and no one is really using floating  point operations to do SNARK computations. We mostly care about integer  arithmetic for prime fields, and then doing bit level operations  for masking in order to do reductions.

So my company Irreducible basically investigates  this problem and is designing custom hardware for SNARKs and actually for a specific SNARK. So  one thing that we learned early on is if you're opinionated about what the protocol is so that  you can adapt the hardware really well to that use case. Some of the things that we've learned  is that there is a type of finite field that is very amenable to cryptography called binary  fields that are extremely good in hardware. We talked in our last segment a bit about the  cryptographic advantages of binary fields, but also there's hardware level advantages. So what are binary fields? They're a  different type of mathematical object.

And so Baby Bear, which is what we  began this session talking about, is an example of a prime field. So  we can put this for example. This is actually the set of integers  modulo a 32-bit prime number. A binary field, there is a 32-bit binary  field that systems like Binius use, and I'll just call this B32. There are  different representations of binary fields, but we'll treat them all as one family for  now. And one thing that we've learned in our investigations is that the hardware efficiency of  doing 32-bit binary field multiplications versus

32-bit prime field multiplications, it's about 5x  more efficient for a binary field multiplication. So here we're saying the number  of CPU instructions to do one field multiplication is smaller here than there. Actually no, it's more interesting than that.  This is talking about if we write a digital logic circuit, so we're not talking about CPU  cycles anymore, we're going to write a native Boolean digital logic circuit, which is going  to get created with transistors and gates, that the number of gates required to  perform this computation at a certain clock speed is five times lower than this.  The number of gates at the same clock speed?

Yes. And is this a secret? Actually no, this  has been well understood in cryptography, and that's why binary fields show up in standards  like AES and in various error correcting codes. So is this a pure win? Well actually we have found  that it's really hard to take advantage of this on traditional CPU and GPU architectures. So GPUs  have these dedicated circuits inside of the cores. The GPU cores are just digital circuits that do  integer multiplication. They do not do binary field multiplication, and they don't have  instructions that are particularly helpful for that. So even though at the native digital  circuit level binary fields have this advantage,

when you're trying to use widely  available hardware today, it actually makes a lot of sense to stick with prime fields. However, if we're talking about the future  of creating custom hardware for SNARKs, we think binary fields have these  advantages. So what else can we do before we have these chips?  There are devices called FPGAs. Field Programmable Gate Array. Field Programmable Gate Arrays.

I'm not going to write that all out, but it's  basically reconfigurable hardware. So Gate Arrays is, as we can think, a bunch of gates, and we  can choose what gate it is and how they're wired? Yes, and you can actually write the same  sort of hardware description language, the same sort of hardware designs that you  might go and send to a foundry or a fab and get back an actual chip. You could instantiate  these on an FPGA with some modifications. These devices are typically or  they're commonly used for prototyping in order to test out your circuit designs,  but in special cases where consumer hardware doesn't do an operation that efficiently,  you can actually run them in production.

What we've seen is the field multiplication  throughput of binary fields that we're able to achieve in an FPGA rivals the field  multiplication throughput of consumer GPUs using prime fields, which is  pretty remarkable that you have this hardware that's designed for a specific  purpose and that reconfigurable hardware, by choosing a different underlying  mathematical object, could compete with that. I see. So if you ported that  same design to an actual ASIC, you expect to see it to be faster than the GPU.

Yes, and ultimately, why is this? When  you put things into a chip and you get rid of the reconfigurability, you're able  to produce an extremely power-efficient and scalable design at the expense of it takes  a while to validate this hardware design, it takes a while to get it back from a fab,  and it's quite expensive. So I think when the space matures and stabilises, we will end up with  custom hardware to do these types of operations. That makes sense. So one thing you might ask is,  am I talking about ASICs here? What is an ASIC?

We've seen those in Bitcoin mining or  Application-Specific Integrated Circuits. We actually probably want more flexibility  than an ASIC. An ASIC is a specific circuit. We can get more flexibility by building  something that's like a GPU architecture, but with an instruction set that's adapted to  what we need. So why is this more flexible? It means that we can write software that  our custom processor is going to read in and do some type of configurable operation, but  instead of configuring it at the circuit level, we're configuring it by making it  a software programmable processor.

I've read these referred to as ASIPs,  Application-Specific Instruction Processors. So can we think of it also as like a  Binary field Processing Unit, like a BPU? Yes. Cool. So that's one way that we could get the advantages of custom  hardware with some flexibility. That makes sense. I think  that's everything I have to say about high-performance engineering for SNARKs. Amazing. So we have parallelism.  Eventually we can't throw more machines because we're limited by memory.

Then we have these pipelining ideas, and then we  saw how to split up witness generation and how to split up even just the whole statement  and make smaller proofs to be able to cluster-level parallelism and then aggregate  all this. And then custom hardware. Exactly. Cool. Thank you very much, Jim. Thank you. This was a pleasure.