Subtitles section Play video Print subtitles DAN MOLDOVAN: Thank you all for coming. My name is Dan Moldovan. And today I will talk about some of the internals and functionality of AutoGraph. Now, this is definitely not an introductory talk. And if you would like to learn more about the background or the motivation behind AutoGraph, here are a few resources that I believe can help. The talk will be otherwise fairly fast paced, quite dense. I'm hoping we'll be able to get through all of it in time. But if not, I'm hoping the slides will be able to serve as a good reference, should you decide to come back and look at everything more closely. I should caution, though, that I am oversimplifying a lot of things for the sake of brevity and time. But the essential things are in there. The talk will be structured in roughly in three parts. First I'll talk some about some of the more relevant implementation details, which are useful to understanding some of AutoGraph's behavior. Then I'll describe the various ways in which you can interact with it. And lastly, I'll go through various use cases that highlight what works, what doesn't work, common pitfalls, how to stay away from them, and what are our plans to eventually address them. So let's begin with the implementation. From a systems perspective, this is roughly what AutoGraph looks like. In broad strokes, we have the following. Going from the bottom to the top, we have an infrastructure for performing source code transformations with various helpers. And on top of that, we have individual transformations. For instance, there is a separate transformation that handles function calls. Another one handles break statements. And yet another transformation handles if statements. And these transformations are independent and composable. Many of these transformations then replace your code with calls to special AutoGraph functions. We call them overloads or operators. The is reason that they are similar to Python's overloaded operators. Now, of those overloads, there are the most interesting ones, the ones that specialize on creating TensorFlow ops. And lastly, there's a high-level API that glues them all together. And this is typically what you usually interact with as a user. One last note that I should make is that of all these pieces, only the TensorFlow specialized overloads and perhaps the high-level APIs, only these are specific to TensorFlow. Everything else is fairly generic and reusable, and we hope to eventually have them in a separate library that can be used for other purposes as well. So one of the fundamental pieces of AutoGraph is, of course, the source code transformation bit. So let's look at that a bit more closely. Source code transformation is essentially what makes AutoGraph a transpiler. It's unit of work is functions. That is at runtime, a function is being converted into a new function. So let's look more closely at that process. It is roughly, loosely speaking, a five-step process. The first step is to obtain the source code of the function. Now, the standard Python library makes that easy for us. It provides that inspect module, which is built in, and it lets us do that. This also highlights one of the fundamental requirements of AutoGraph. In order to convert a function, that function must expose its source code. And that's typically true for almost all functions in Python, although there are certain exceptions. Normally, you can test this on your function by calling the inspect get source. If inspect get source returns data, then AutoGraph should be fine with it. The second step in this process is to parse the code into an AST. And once more, there is a standard Python API for this, which is good. We, in fact, use a thin layer on top of that. It's a third-party library called Gast. It's practically identical to AST, but it handles all the version differences between Python 2 and Python 3. It's worth mentioning at this point that AutoGraph operates entirely at AST level. There is no lower-level intermediate representation. And we never interact with the bytecode. And that has some unique advantages. Now, the third step does the bulk of the work. And that's quite both literally and figuratively. The standard Python library offers a mechanism that helps us with that as well. The AST module provides a mechanism for visiting and transforming ASTs. That mechanism uses the visitor pattern, and it's sketched here. Basically you get some callbacks whenever the visitor encounters different types of nodes. And on top of that, we have built an entire library of such transformations, as we've seen in the previous diagram. These transformations are called in sequence. Now, once transformed, the AST is unparsed back into source code in the form of a string. There isn't a standard library for doing that. But thankfully there's a third-party library called Astor, which does a decent job of that. Essentially, it's lots of string concatenations. There's nothing special about that. Finally, the source code is being outputted into a file and then loaded using a mechanism that's identical to writing an import statement. Once more, Python helps us with that, with the standard module called Imp. The special thing about Imp is that it only works with files on disk, hence the need to generate a temporary file. I should also make a slight note that another mechanism that we could have used would be Exec. And we've been going back and forth between using that and Imp. There are pros and cons to using each. So we might revisit this in the future. A few other mechanisms that are worth mentioning, one of them is the templating system that we developed to help us with generating code. It essentially lets us write templates in the form of strings, code blocks of strings. And they support placeholders, and they let us generate more complex or new ASTs. If you ever poke inside the transformations library, you will see plenty of such templates. Another important piece is the static analysis, which is critical in supporting certain transformations, and we'll see more about that in a bit. The analysis itself can be viewed as just a simple walk over the AST. And it annotates nodes with relevant information. Another important mechanism is caching. Caching itself is completely transparent to the user. But it does help us make sure that every function is converted no more than once, loosely speaking. This cache relies on the key assumption that the conversion process is entirely static. That is the generated code, what ends up in the generated code does not depend on any arguments or variables or any other state of Python. Basically, if you look at some plain code on paper, you would know exactly what the output code should be. Next let's talk about some of the actual transformations that are being made. And before I proceed, I should clarify that I'll use the word variable a lot. These are meant to mean Python variables, not to be confused with TensorFlow variables, which are not involved here. Once such transformation is simplifying the code by replacing some statements with other statements, simpler ones. Such as, for instance, we replace break statements with variables and additional if statements. This essentially helps us avoid the need to add to build special handlers in TensorFlow for these statements, like break. They're just easier to lower into something simpler.