RPC

Kamodo includes a Remote Call Procedure (RPC) interface, based on capnproto. This allows Kamodo objects to both serve and connect to other kamodo functions hosted on external servers.

We can test this functionality using docker compose (See the docker-compose.yaml file in the base of this repo).

Our docker-compose configuration contains two services we can use to test this functionality:

  • kamodo-rpc-py37 (runs kamodo-core with python 3.7)
  • kamodo-rpc-py38 (runs kamodo-core with python 3.8)

Here are the corresponding definitions in docker-compose.yaml:

  kamodo-rpc-py37:
    build:
      context: .
      dockerfile: dockerfiles/kamodo-rpc-py37.Dockerfile
    volumes:
      - type: bind
        source: ${PWD}
        target: /kamodo-core
    ports:
      - "60001:60000"
  kamodo-rpc-py38:
    build:
      context: .
      dockerfile: dockerfiles/kamodo-rpc-py38.Dockerfile
    volumes:
      - type: bind
        source: ${PWD}
        target: /kamodo-core
    ports:
      - "60000:60000"
    command:
      - kamodo-rpc
      - rpc_conf=kamodo/rpc/kamodo_rpc_test.yaml
      - host=0.0.0.0
      - port='60000'

Either one will mount the base of the repo into the corresponding container at run time. Note that kamodo-rpc-py37 will use port 60001.

Configuring a Kamodo server

To configure a Kamodo server, create a kamodo_rpc.yaml file. A simple example is given in the root of the kamodo-core repo:

!Kamodo
f: x**2 - x -1

The !Kamodo line informs our yaml parser that the next line defines a kamodo object with the following key-value pairs.

Note

The kamodo specification file is under rapid development

Starting an Kamodo server

To build a container compatible with python 3.7, run

docker compose build kamodo-rpc-py37

This will pull the latest version of kamodo-core and build it inside a container ready for deployment. Feel free to build on top of the resulting image.

Next, start the container with:

docker compose up kamodo-rpc-py37
Click to expand console output
[+] Running 1/0
 ⠿ Container kamodo-core-kamodo-rpc-py37-1  Created                                                                     0.0s
Attaching to kamodo-core-kamodo-rpc-py37-1
kamodo-core-kamodo-rpc-py37-1  | {'rpc_conf': 'kamodo/rpc/kamodo_rpc_test.yaml', 'host': '0.0.0.0', 'port': '60000'}
kamodo-core-kamodo-rpc-py37-1  | serving   arg_units lhs           rhs symbol units
kamodo-core-kamodo-rpc-py37-1  | f        {}   f  x**2 - x - 1   f(x)      
kamodo-core-kamodo-rpc-py37-1  | certfile not supplied
kamodo-core-kamodo-rpc-py37-1  | keyfile not supplied
kamodo-core-kamodo-rpc-py37-1  | using default certificate
kamodo-core-kamodo-rpc-py37-1  | Using selfsigned cert from: selfsigned.cert
kamodo-core-kamodo-rpc-py37-1  | [2022-05-04 22:23:23,543][rpc.proto][DEBUG] - Try IPv4

At startup, the container automatically generates an SSL key and certificate (when one is not already present).

Starting a client

Since the root of the repo is mapped into the container, we can access the cert from the host selfsigned.cert.

Open a separate terminal and connect to the container from your host system (use the same directory where selfsigned.cert i.e. the root of the mounted kamodo-core repo):

In [1]: from kamodo import KamodoClient
In [2]: k = KamodoClient(port=60001)
In [3]: k.f(5) 
Out[3]: 19 # (5)**2-(5)-1

RPC Spec

Kamodo uses capnproto to communicate binary data between functions hosted on different systems. This avoids the need for json serialization and allows for server-side function pipelining while minimizing data transfers.

Kamodo's RPC specification file is located in kamodo/rpc/kamodo.capnp:

@0xbfd16a03c247aaa9;


interface Kamodo {
  struct Map(Key, Value) {
    entries @0 :List(Entry);
    struct Entry {
      key @0 :Key;
      value @1 :Value;
    }
  }

  getFields @0 () -> (fields :Map(Text, Field));

  getMath @1 () -> (math :Map(Text, Function));

  evaluate @2 (expression: Expression) -> (value: Value);

  interface Value {
    # Wraps a numeric value in an RPC object.  This allows the value
    # to be used in subsequent evaluate() requests without the client
    # waiting for the evaluate() that returns the Value to finish.

    read @0 () -> (value :Literal);
    # Read back the raw numeric value.
  }

  struct Expression {
    # A numeric expression.

    union {
      literal @0 :Literal;
      # A literal numeric value.

      store @1 :Value;
      # A value that was (or, will be) returned by a previous
      # evaluate().

      parameter @2 :UInt32;
      # A parameter to the function (only valid in function bodies;
      # see defFunction).

      call :group {
        # Call a function on a list of parameters.
        function @3 :Function;
        params @4 :List(Expression);
      }
    }
  }

# Void: Void
# Boolean: Bool
# Integers: Int8, Int16, Int32, Int64
# Unsigned integers: UInt8, UInt16, UInt32, UInt64
# Floating-point: Float32, Float64
# Blobs: Text, Data
# Lists: List(T)


  struct Literal {
    union {
      void @0 :Void;
      bool @1 :Bool;
      int8 @2 :Int8;
      int16 @3 :Int16;
      int32 @4 :Int32;
      int64 @5 :Int64;
      uint8 @6 :UInt8;
      uint16 @7 :UInt16;
      uint32 @8 :UInt32;
      uint64 @9 :UInt64;
      float32 @10 :Float32;
      float64 @11 :Float64;
      text @12 :Text;
      data @13 :Data;
      list @14 :List(Literal);
      array @15 :Array;
      int @16 :Text;
      listint64 @17 :List(Int64);
      listfloat64 @18 :List(Float64);
      rational @19 :Rational;
    }
  }

  struct Rational {
    p @0 :Int64;
    q @1 :Int64;
  }


  # everything needed for registration
  struct Field {
    func @0 :Function;
    meta @1 :Meta;
    data @2 :Array;
  }

  # match kamodo's meta attribute
  struct Meta {
    units @0 :Text;
    argUnits @1 :Map(Text, Text);
    citation @2 :Text;
    equation @3 :Text; # latex expression
    hiddenArgs @4 :List(Text);
  }

  struct Argument {
    name @0 :Text;
    value @1 :Literal;
  }

  # needs to be an interface
  struct Array {
    data @0 :Data;
    shape @1 :List(UInt32);
    dtype @2 :Text;

  }

  interface Function {
    # A pythonic function f(*args, **kwargs)
    call @0 (args :List(Literal), kwargs :List(Argument)) -> (result: Literal);
    getArgs @1 () -> (args :List(Text));
    getKwargs @2 () -> (kwargs: List(Argument));
  }

}

The above spec allows a Kamodo client (or server) to be implemented in many languages, including C++, C# (.NET Core), Erlang, Go, Haskell, JavaScript, OCaml, and Rust.

Further reading on capnproto may be found here: