← All AI Engineer talks

AI Engineer Code 2025

Compilers in the Age of LLMs

Yusuf Olokoba· Founder, Muna17:36

Read the talk

Compilers in the Age of LLMs

Follow an embedding function from Python through tracing, type propagation and native compilation, then call it from Node.js through an OpenAI-style client.

From a talk by Yusuf Olokoba

Before you start: Basic familiarity with Python functions, type annotations, and model inference will help; compiler and FFI concepts are introduced as they arise.

One more model, one more deployment

An OpenAI client in the codebase, Hugging Face tabs in the browser, several playground repositories, and an agent that strings together HTTP calls: this is a familiar starting point for an AI application. Adding a new open-source model often means writing another Dockerfile, running another container, and finding infrastructure to host it. Making that model available to an agent adds another tool to its context, perhaps exposed through MCP. The model may be easy to download; adopting it still expands the application’s infrastructure.

The desired interface is much smaller: an OpenAI-style client that accepts a model name and works with minimal application changes. Execution might be local or remote, backed by llama.cpp or TensorRT. Yusuf Olokoba’s proposed route to that experience is a Python compiler: take plain inference code and turn it into a self-contained native artifact for targets ranging from cloud servers to Apple Silicon. His introduction also frames LLM-generated code as something to constrain with verification and testing.

Two requirements motivate that choice. First, developers should be able to bring an internal model or one found on Hugging Face or GitHub and obtain something their application can execute. Changing the model argument when a new OpenAI model appears provides the interaction to emulate. A compiler supplies the missing transformation: Python inference code goes in; an artifact suited to the user’s execution environment comes out.

Second, Olokoba expects hybrid inference to become common: small models on devices or at edge locations cooperate with larger cloud models that offer stronger reasoning. That forecast changes the deployment target. A server-side Python process in a Docker container is no longer sufficient for every location where inference should happen. Native execution offers a route closer to the hardware, with fewer runtime dependencies and greater control over responsiveness.

0:000:21
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

0:00 · section reference included

Start with a Python embedding function

The running example is Google’s EmbeddingGemma. A Python function takes a list of sentences and returns an embedding vector for each sentence—together, an embedding matrix. Those vectors support text search, retrieval-augmented generation, and retrieval of individual document passages. Olokoba describes the model as having 270 million parameters; Google’s release specifies 308 million. His reason for choosing it is its suitability for both cloud GPUs and consumer devices, rather than a measured speed comparison in this demonstration.

The intended journey has three stages: translate the Python function into equivalent C++ or Rust, compile it with the model and its dependencies, and expose inference through client.embeddings.create. Before generating native code, however, the compiler needs a graph describing what the function does. That graph becomes the basis for translating the function operation by operation.

Diagram connecting Python, C++ / Rust, a self-contained executable, and OpenAI client embeddings with arrows.
From Python through C++ or Rust to a self-contained executable and OpenAI client embeddings.
4:024:26
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

4:02 · section reference included

Finding a practical way to capture the function

The first prototypes used PyTorch’s compiler tooling, including torch.compile and torch.fx. Two details help separate the tools: FX arrived in PyTorch 1.8, before PyTorch 2, and its basic symbolic tracer records operations through Proxy objects. Fake tensors, which carry tensor metadata without allocating the represented data, belong to related compiler analysis machinery. Olokoba describes his prototype in terms of running with fake inputs to obtain an operation graph.

That approach exposed two problems for the team’s broader Python target:

  • Library coverage: inference functions may include NumPy operations, OpenCV calls, and other non-PyTorch data types. Supporting those paths would require extending the tracing machinery.
  • Input representation: a data-free tensor can retain a tensor’s shape and other metadata, but constructing suitable stand-ins for images, dictionaries, and arbitrary objects was harder in their prototype.

These were obstacles to the team’s chosen implementation, rather than a universal requirement that every FX trace use fake inputs.

The next experiment asked an LLM to produce the trace directly. Structured outputs supplied the constraint: give the model source code and a response schema, then ask for a graph in that schema. Olokoba reports almost 100% accuracy for LLM trace generation in internal testing, but says it took too long. He supplies neither the test size and correctness criterion nor a latency measurement. The limiting factor was therefore the reported cost in time, even when the generated structure looked promising.

The eventual tracer instead analyzes Python’s abstract syntax tree, using internal heuristics to construct an intermediate representation, or IR. For the embedding example, the relevant graph contains the string-list input, a tokenizer call, a model call, and the returned embedding vectors. This gives the compiler a structured description of the computation without making an LLM request the tracing mechanism.

5:315:48
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

5:31 · section reference included

Propagating types through the graph

An operation graph is not yet enough to emit native code. Python can bind x to an integer and then rebind it to a string. An individual variable in the generated C++ or Rust has a fixed type, whether that type is written explicitly or inferred. The compiler must therefore determine native types for the intermediate values in the Python function.

The example begins with a list comprehension that prepends a task-specific prefix to each input sentence. The essential operation can be isolated in Python like this:

python

def make_prompts(text: list[str], prefix: str) -> list[str]:
    return [prefix + sentence for sentence in text]

In the demonstrated function, the prefix comes from a global task-prefix map. The function’s annotation establishes that each member of text is a string, and the map supplies string-valued prefixes. The remaining question is the native type of prefix + sentence.

Type propagation answers that question using an implementation of the operation for the known operand types. Here, Python’s operator.add means string concatenation. A corresponding C++ operation can have this signature and body:

cpp

#include <string>

std::string add_strings(
    const std::string& prefix,
    const std::string& sentence
) {
    return prefix + sentence;
}

The function takes two strings and returns a string. Its return type supplies the missing type information for the graph node.

ValueWhere its type comes fromKnown type
Input sentencePython input annotationString
Task prefixGlobal constant mapString
Concatenated promptNative addition resultString

The compiler combines information from the Python function signature with native type information for constants and operations. Once the concatenation result is known, that information can flow onward through the comprehension and subsequent operations. Repeating the process determines types for intermediate values across the IR.

8:168:28
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

8:16 · section reference included

Use LLMs to build the operation library

Type propagation moves a substantial requirement into view: every supported Python operation or function call needs a corresponding native implementation. Knowing how string addition behaves does not supply the implementations for other operand types, library functions, or tensor operations. Olokoba accepts that requirement; the compiler needs those building blocks.

His first reason for considering it tractable is that source-code variety comes largely from combinations of operations. Many different functions reuse the same elementary operations in different sequences. The slide illustrates this with operator.add, operator.pow, and operator.matmul: Function A and Function B contain the same operations in different orders. Native implementations can likewise be composed into different generated functions.

Function A lists operator.add, operator.pow, and operator.matmul; Function B lists operator.pow, operator.matmul, and operator.add.
Different functions combine the same underlying operations in different orders.

The elementary set is still large. Addition, subtraction, and exponentiation are only the beginning; useful inference functions also rely on NumPy and PyTorch operations. This is where LLMs return to the compiler design. Instead of requiring engineers to handwrite each native equivalent, the team uses LLMs to generate C++ and Rust implementations. The role is narrower than generating a fresh trace for every user function: produce reusable implementations of operations that the compiler can compose.

11:3511:55
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

11:35 · section reference included

Emit native source and compile a shared library

With native operation implementations available and types propagated through the graph, the compiler has the information needed to emit typed C++ source. Olokoba presents this as the point where the IR can become compilable native code. The side-by-side walkthrough follows the original computation: build the prefixed prompts, tokenize them into IDs, run the model, and return the embedding matrix. The deployment language changes while the sequence of inference operations remains recognizable.

The next step uses an ordinary native compiler. C and C++ toolchains exist across a broad range of devices, which makes native source a useful portability layer. The result in this example is a dynamic library, or shared object, that another process can load and execute. Portability here means building for supported targets, not shipping one native binary that executes on every architecture; Olokoba’s companion article also describes target-specific builds and incomplete Python language coverage.

13:3413:50
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

13:34 · section reference included

Call the compiled model from Node.js

The host application in the demonstration is JavaScript running on Node.js. A foreign-function interface, or FFI, bridges JavaScript and the compiled library. The binding must identify the library built for the host’s operating system and architecture, name the exported function, and declare its native signature. Those declarations tell the binding how to call the function across the language boundary.

The invocation procedure is then straightforward:

  1. Locate the compiled library.
  2. Load it through the FFI bindings.
  3. Invoke the exported inference function with the expected inputs.
  4. Receive the embedding matrix.

The team standardizes this scaffolding across compiled functions. The particular binding implementation remains open-ended in the explanation; the demonstrated result is a JavaScript call that obtains embeddings from the native library.

15:0215:15
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

15:02 · section reference included

Put the familiar client interface around native inference

The final layer restores the interface that motivated the compiler. A Client class contains an Embeddings class whose create method exposes the familiar client.embeddings.create path. That method receives the model name and resolves it to the path of the compiled binary. The FFI machinery already built for Node.js loads the library and executes its inference function.

One step remains after execution: reshape the embedding matrix into the result structure expected from the OpenAI-style client. That separates three responsibilities cleanly: model-name resolution chooses an artifact, FFI invokes native inference, and response conversion presents the result to the application. The final slide brings these layers together with an embedding request containing a question about the capital of France.

Code slide showing a Client class, an Embeddings create method with library lookup, execution and compatibility conversion, and an embedding request asking the capital of France.
Client and Embeddings classes wrap library execution and output conversion, followed by an example call.

The application can now request embeddings through a familiar method while the implementation runs a compiled open-source model inside the host process. Olokoba closes with the ambition of extending this access to any open-source model expressible through a Python function. Within the compiler’s supported operations and build targets, the completed example shows what that means concretely: a model name selects a native library, the library returns embeddings, and the client presents them in the expected shape.

16:0616:15
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

16:06 · section reference included

Resources

From the talk

  • Google's launch announcement for its 308M-parameter embedding model, with on-device inference conditions and integration links.

Updates since the talk

  • Current guidance for preparing Python functions for Muna compilation, including annotations, supported types and language coverage.

Read the complete timestamped transcript
  1. 0:00

    If you're an AI engineer right now, your day-to-day probably looks something like this. You've got an OpenAI client in your code base, you've got a few Hugging Face tabs open, you've got three different repos with the word playground in them, and you've got at least one agentic workflow that's really just stringing together a bunch of HTTP calls.

  2. 0:21

    Right now, everyone is talking about voice agents, MCP, and these are pretty cool technologies. But when you peel back the hype a little bit, what I hear when I talk to a lot of engineering teams is that they're usually grappling with much more fundamental and boring problems.

  3. 0:38

    How do I use more models in more places without having to rebuild or extend my infrastructure every single time? So say you wanna go try out a new open source model that just dropped on Hugging Face.

  4. 0:52

    Today, that usually means you gotta go write a Docker file, spin up a Docker container, and then get that running on infrastructure that you own or that you rent from a third-party provider.

  5. 1:03

    And if you're wiring this into an AI agent, well, that's another tool that you have to put into the context and perhaps expose either like an MCP or something similar.

  6. 1:13

    A lot of this is just complexity that creeps in and only grows further more time you spend. What developers actually want is something way simpler. Just give me an OpenAI style client that just works.

  7. 1:28

    Let me point it to any model at all. It doesn't matter if it's running locally, if it's running remotely, if it's llama.cpp or TensorRT, I just want something that works with minimal code changes.

  8. 1:40

    In this talk, I'll walk you through how we decided to build a compiler for Python that enables developers to write simple, plain Python code and then convert that into a tiny self-contained binary that can then run anywhere at all.

  9. 1:57

    It could be the cloud, it could be Apple Silicon, it could be anything else in between. Further, I'll show you how we use LLMs within that compiler pipeline. A few things we tried, what worked, what didn't work, how we fenced them with verification and LLM power testing, and how these-- this infrastructure gives us the ability to not

  10. 2:15

    just run any AI model at all, but we can now run it in so many more places beyond just server side.

  11. 2:22

    So before we start getting our hands dirty with an example, I wanted to provide some motivation on why we thought building a Python compiler was the best way to solve AI deployment in the long run.

  12. 2:35

    First, we needed an extremely simple and standardized way for developers to bring their AI models, whether the ones that they've built internally or models that they found open source on Hugging Face on GitHub, and then get something that they could execute very easily in their code base.

  13. 2:54

    So when a new OpenAI model comes out, for example, all you have to do is simply just change the model argument, pointing it to the new model that OpenAI just dropped.

  14. 3:03

    We wanted to recreate something that tracked this experience as closely as possible. Conceptually, this would have to look like something that ingested code, Python inference code, and then spat out some other thing that knew how to get executed in our develop-- in our users', uh, execution environments.

  15. 3:22

    Second, we wanted to prepare for what we strongly believe to be the future of AI deployment, hybrid inference. We expect that in the future we will see smaller models, typically much closer to users, either locally on their devices or in edge locations, working in tandem with cloud AI models that are much larger and have a, a bigger

  16. 3:43

    reasoning abilities. And we expect that this is going to be the future of how a lot of people consume AI in their day-to-day lives. As such, this means that developers have to move away from, you know, the, the cages of Python code and Docker containers into something that is a lot more low level, closer to the hardware,

  17. 4:02

    and a lot more responsive. So let's get our hands dirty. This is a Python function that runs Google's EmbeddingGemma two seventy million parameter model. It's a very simple text embedding model that takes in a list of, of sentences, just plain text, and then runs a model that is able to generate an embedding vector or a list of

  18. 4:26

    embedding vectors, an embedding matrix. You will typically use models like this in text, in text search, in retrieval-augmented generation, and in other frameworks where you need to be able to retrieve documents or retrieve subsections of documents.

  19. 4:41

    This model from Google is small enough at only two hundred and seventy million parameters that not only can it run very easily on, uh, GPUs in the cloud, it can also run very quickly on consumer hardware also.

  20. 4:53

    And today we will be figuring out how to take this Python function that runs the embedding model,

  21. 4:59

    generate equivalent C++ and Rust code that is much lower level and is now able to run anywhere at all. And then we will compile a binary that contains this model and all the dependencies it needs.

  22. 5:12

    And finally, we will consume this model using the familiar openai client.embeddings.create experience. The very first step is taking our function and generating a graph representation that describes everything that happens within that function.

  23. 5:31

    We call this tracing. Initially, our, uh, first prototypes of building a symbolic tracing, uh, solution was actually built off of PyTorch 2, which introduced torch.compile along with torch.fx, uh, for this purpose.

  24. 5:48

    So the way that torch.fx works is it'll take in Python source code and then run it with fake inputs that don't allocate any memory, and then give you a description, a graph of everything that happened within that function.

  25. 6:02

    We actually tried to use this, but we faced two major issues that caused us to build our own, uh, tracing infrastructure. The first was that PyTorch, uh, is very focused.

  26. 6:14

    Its tracer is very focused on only PyTorch code. And so in order to trace arbitrary code, which your functions will usually have to rely on, things like NumPy operations or OpenCV or something else, we would have had to figure out a way to like add support for those data types into PyTorch.

  27. 6:32

    The second reason why we didn't stick with PyTorch was in order for the tracer to work, it had to be run on fake inputs. And so, you know, creating a fake tensor is trivial.

  28. 6:44

    You just, you know, give it the same description and don't allocate any data. But it's a lot harder to create a fake image or a fake dictionary or a fake, you know, whatever type that we might encounter in the wild.

  29. 6:56

    And so we simply decided that we were gonna build something in-house. Our first attempt was actually using LLMs as a way to generate traces because LLMs for quite some time now have had this capability of structured outputs.

  30. 7:11

    This is where you can give an LLM a prompt, some data, whether it be an image, text, or audio, and ask it to respond to you with the specific schema that you have given to the model.

  31. 7:21

    This actually turned out to work pretty well. Uh, it had almost like a hundred percent, uh, accuracy rate in our own testing. The only limitation was it simply took way too much time.

  32. 7:32

    And so eventually, we decided we're just gonna do it old school. We would build a tracer by first analyzing the code, looking at the AST or the ast-- uh, the abstract syntax tree of the Python code, and then using a bunch of internal heuristics to build our own internal representation or IR of the user's function.

  33. 7:51

    So for this function that we've written up, the IR is actually incredibly simple. I'm not gonna show you the entire thing, but I'll just show you the parts that are relevant to look at.

  34. 7:59

    As you can see, there's input nodes for the actual, uh, inputs to the function, so like that's a list of the strings. There's a function call to calling out to the tokenizer, another one's calling out to the model, and then return those outputs so that the user can then get their embedding vectors.

  35. 8:16

    Now that we have a high-level intermediate representation of our Python function, the next step is to figure out how to translate that somehow into lower level C++ or Rust code.

  36. 8:28

    But before jumping into that, I wanted to talk about one major difference between Python as a language and C++ or other lower level languages that we will run into and have to solve.

  37. 8:40

    Python is a very dynamic language, so one variable, X, could be assigned to an integer and then immediately after assigned to, say, a string. There is full dynamism and anything goes.

  38. 8:54

    Whereas in lower level languages like C++ and Rust, if you declare a variable, you must give it a type, and that type can never change. This gives us quite a bit of a challenge because we need to figure out how to attach or constrain the types in the code that we will be generating from our Python high-level

  39. 9:13

    code. So let's look at the first line of our function, the very first node, if you call it, of our IR. As you can see, prompts is this list that is being generated by a comprehension statement, and we're effectively just adding a set of prefixes for every sentence that has been passed in by the user.

  40. 9:35

    And so let's just focus in on that addition operation that's happening within that comprehension. As you can see, well, we know that every item in text is a string because we have pretty much annotated our function as such, right?

  41. 9:50

    The input text is a list of strings. And we also know that the text prefix map just contains a bunch of strings. Each prefix is itself a string. And so the question then becomes: How do we know or how do we figure out the C++ type on the output of that operation?

  42. 10:07

    And this is where the compiler comes in, specifically a technique we call type propagation. And so here we will take one string, the prefix, the other string, the actual input text that was provided to the function, and we now know that there is some addition operation happening to these two.

  43. 10:26

    So we can simply write or generate a C++ function that takes in two strings and performs the operator.Add operation from Python. The output of that function that we generate in C++, as you can see here, well, it's just a string.

  44. 10:44

    And that's how we know that whatever the output of this addition operation, uh, we're doing is must itself be a string. So in that way, zooming out, we've been able to take the input information, the input type information from just the signature of our Python function, along with the C++ type information or the, the native type information

  45. 11:08

    of this global constant task prefix map, and then we've been able to use that to propagate into the output of the concatenation of these two things. We now know that if I concatenate one prefix with one input string, the result itself is a string.

  46. 11:24

    And so we can then do this propagation for every intermediate variable or every operation within our original Python function, and that's how we can kind of like flow type information through.

  47. 11:35

    And so at this point, you might be wondering, well, your compiler, if you're doing this propagation thing that requires us manually implementing some operation in C++ or in Rust code, we would have to literally rewrite this for every unique p- function call or operation that we ever encounter in Python.

  48. 11:55

    And you'll be correct. You'll be a hundred percent correct. That is in fact what we would have to do. But that is now tractable, and it's an easier problem to solve now for two reasons.

  49. 12:07

    The first reason is that all the variety you will ever see in source code in the wild is not because there is such a giant volume of these operations.

  50. 12:17

    The volume is actually because you can combine operations in so many different ways. You can permute them in so many different ways, and each of these permutations is what forms a unique Python function or Python code.

  51. 12:30

    And so we really only need to cover that base level or that base number of elementary functions, and we could just stack them or combine them in different ways in C++ the same way we do in Python.

  52. 12:42

    But you might even say to that, that wait, that elementary set of functions, it's still pretty large, and you would be 100% right. We need to cover everything from, you know, adding two things to like, you know, subtracting them to exponentiation to like, you know, some stuff that is like in native libraries like NumPy operations or PyTorch

  53. 13:01

    operations. And so yeah, so you have a, a perfectly valid point. The only reason why that's tractable now is, well, we don't have to sit down and write the equivalent native code that does the same thing in Python anymore.

  54. 13:14

    We can simply have LLMs generate all the code that we need that translates a function from Python, right, into C++ and Rust. And so this gives us the ability to basically mass produce a lot of the operations that we, we would otherwise have had to manually rewrite ourselves in native code.

  55. 13:34

    And so now that we've been able to propagate type information through our Python IR graph, we basically have all we need to simply generate actual C++ code that is correct and will compile.

  56. 13:50

    So here's what it actually looks like side by side. As you can see, I'm literally just walking through, and you can see where we're doing that, you know, list comprehension to add the prefixes to each string.

  57. 14:00

    You can see where we are running the tokenizer to tokenize those input text into IDs, and you can now see we're running the model and returning the output embedding vectors or the embedding matrix.

  58. 14:13

    At this point, because we now have C++ source code, we can now compile this to run natively on any device or platform that we would ever want to run on simply because every piece of technology that you've ever touched has a C or C++ compiler.

  59. 14:30

    This is what gives us the ability to take high-level Python code and convert it into a form that is self-contained and that can now run anywhere at all. So let's go ahead and do that, and then what we're gonna end up with on the other end is simply a s- uh, dynamic library, uh, a shared object, if

  60. 14:47

    you call it that, that we can then load into a process and execute like any other code. Now comes the fun part. Let's figure out how to actually invoke or use our compiled embedding model from any language on any device.

  61. 15:02

    We're gonna go with JavaScript running on Node.js for this example. And so the very first step we wanna do is figure out how to call in to our compiled library from JavaScript in Node.js.

  62. 15:15

    We can use FFI for this, for this purpose, and so this is where you're able to effectively design bindings and declare that, "Hey, I'm loading this native library, which has been compiled from my system and my architecture.

  63. 15:29

    It has this function with some name." In our case, we already have a, a function name. And that function, that native function has this signature. And so we're able to write a bunch of scaffolding code.

  64. 15:41

    This... We've figured out a way to standardize this across different, different, uh, compiled functions to make it very easy for ourselves, but this is pretty open-ended. Once you do, you can basically point Node.js or your JavaScript application to the location of that compiled library, load it in, and simply just invoke it like any other thing.

  65. 16:00

    When we do, guess what? We get our embedding matrix right there.

  66. 16:06

    And for the final piece of the puzzle, let's take it back to the top. Let's figure out how to expose our compiled embedding model through our OpenAI style client.

  67. 16:15

    So what we're gonna do is create a class, just call it Client. Within it, we'll create a nested class called Embeddings, and within that, we will create a create function mirroring the official OpenAI client.embeddings.create path.

  68. 16:30

    And so within that function, when the user passes in the model name, all we're gonna do is simply just go from the name of that model to a path to the compiled binary that we just created from our C++ code generation.

  69. 16:45

    And with that, with the rest of all the, uh, FFI that we just implemented, we now have a way of taking the model, resolving it to a path to the library, loading that library in, uh, library in, and simply just executing it to get out our embedding matrix.

  70. 17:00

    The final step is to simply massage the outputs so that it looks just like the outputs that the official OpenAI client gives you. And with this entire system in place, we have just recreated the official OpenAI client, but given it access to any open source model that we can com- get into a Python function.

  71. 17:21

    So