Comment on page
Anatomy of an Op
Understand when and how to use Contexts and type annotations
import tinychain as tc
# This is a native Python function.
# It doesn't need type annotations, although you can add them if you want,
# for example if they make the code more clear,
# or if you're using a linter which requires them.
#
# Since this is a native Python function, TinyChain code can only call it
# at compile-time.
def example1(b):
a = 2
return b + a, b * a
# This is a TinyChain Op. It can be evaluated at run-time.
# Every TinyChain Op has its own Context, which you can access
# by using the reserved names "cxt" or "txn" as the first argument of the Op.
#
# Since this is meant to be called at run-time, TinyChain doesn't know
# at compile-time what type of arguments to expect. So an Op needs
# type annotations in order to provide the correct API when compiled to JSON.
#
# For example, if you leave out the tc.Number annotation, TinyChain will
# provide a generic tc.State when this Op is compiled, and you'll get an
# error that says that tc.State doesn't support the "+" or "*" operators.
@tc.get_op
def example2(cxt, b: tc.Number) -> tc.Tuple:
cxt.a = 2
return b + cxt.a, b * cxt.a
Notice the type annotation
Number
on the parameter b
. The actual value of parameter b
may not be known when example
is encoded as part of a graph configuration, so the type annotation is necessary in order to provide the correct API methods from inside the Python function definition. For example, if someone calls example(URI("http://example.com/numeric_constant"))
, this is perfectly valid code, but it doesn't explicitly provide the type of the Value
at http://example.com/numeric_constant
.Likewise, the return type annotation
Tuple
is provided for the benefit of the calling code. Without it, TinyChain would not know what type of State
to return at encoding time (when the compute graph configuration is generated) and helper methods like unpack
would be unavailable.The
cxt
parameter in the example
function above is a Context
object. It allows you to explicitly name the states which define your Op
—in this case, there is one state called b
passed in as an argument and one state called a
defined in the body of the Op
. TensorFlow users should find this familiar because it provides the same functionality as the name_scope
function and name=
keyword argument in TensorFlow.Developers new to TinyChain sometimes find it confusing that the name "cxt" is used to refer to many different, independent contexts. Consider this (non-working!) example:
import tinychain as tc
@tc.get_op
def my_constant() -> tc.Int:
return 2
@tc.get_op
def example(cxt):
return cxt.my_constant() * 5 # this is where the error happens!
if __name__ == "__main__":
cxt = tc.Context()
cxt.my_constant = my_constant
cxt.example = example
# note: here's a run-time call to the example op
cxt.product = cxt.example()
This won't work! Calling
example
in this case will raise a NotFoundException
because example
has a completely different and independent Op
context. You can verify this yourself by callingtc.print_json(cxt)
from within the example
function.This should make intuitive sense. If
example
and my_context
were hosted on different servers, would it make sense for the one to be able to access the other's internal state? What about from a security perspective?Let's take another look at the
example
function:@tc.get_op
def example(cxt, b: tc.Number) -> tc.Int:
cxt.a = 2
return cxt.a + b, cxt.a * b
The Python interpreter is imperative and single-threaded, so as a Python developer you're probably accustomed to the assumption that each line of your code will execute in exactly the order that it's written. TinyChain, however, is multi-threaded and features automatic concurrency (like TensorFlow). So, in the example above,
cxt.a + b
and cxt.a * b
both execute simultaneously when a user executes your compute graph.Automatic concurrency is crucial for a performant distributed runtime, but it comes with some trade-offs. For example,
Value
s are immutable:@tc.get_op
def example(cxt, b: tc.Number) -> tc.Int:
cxt.a = 1
cxt.a += b # this is where the error happens!
return cxt.a
This won't work! If it did, it would be impossible for TinyChain to calculate the dependencies of each state in the
Op
context, and thus impossible to resolve independent states concurrently.Only
Collections
like a BTree
, Table
, or Tensor
are mutable. This introduces the need to handle side-effects with the After
flow control, which we'll cover in the next section.Last modified 1yr ago