Server/Client I/O¶
Often you will want to call one model from another like a functions. This would required sending the input variable(s) to the model being called and then sending the output variables(s) back to the calling model. We refer to this as a Remote Procedure Call (RPC). The model being called can be considered a server providing its calculations as a service to the client (the calling model). The yggdrasil provides options for treating models as server and clients.
One Server, One Client¶
In the example below, the “server” model computes the nth number in the Fibonacci sequence and the “client” model calls the server to get a certain portion of the Fibonacci sequences and then writes it to a log. The server will continue processing requests until all clients connected to it have disconnected.
Model Code:
1import numpy as np
2from yggdrasil.interface.YggInterface import YggInput, YggRpcServer
3
4
5def get_fibonacci(n):
6 r"""Compute the nth number of the Fibonacci sequence.
7
8 Args:
9 n (int): Index of the Fibonacci number in the Fibonacci sequence that
10 should be returned.
11
12 Returns:
13 int: The nth Fibonacci number.
14
15 """
16 pprev = 0
17 prev = 1
18 result = 1
19 fib_no = 1
20 while fib_no < n:
21 result = prev + pprev
22 pprev = prev
23 prev = result
24 fib_no = fib_no + 1
25 return result
26
27
28def main():
29 r"""Function to execute server communication and computation in a loop."""
30
31 print('Hello from Python server!')
32
33 # Get parameters
34 inp = YggInput("params")
35 retval, params = inp.recv()
36 if not retval:
37 raise RuntimeError('server(P): ERROR receiving parameters')
38 print('server(P): Parameters = %s' % params)
39
40 # Create server-side rpc conneciton using model name
41 rpc = YggRpcServer("server", "%d", "%d")
42
43 # Continue receiving requests until the connection is closed when all
44 # clients have disconnected.
45 while True:
46 print('server(P): receiving...')
47 retval, rpc_in = rpc.recv()
48 if not retval:
49 print('server(P): end of input')
50 break
51
52 # Compute fibonacci number
53 n = rpc_in[0]
54 print('server(P): Received request for Fibonacci number %d' % n)
55 result = get_fibonacci(n)
56 print('server(P): Sending response for Fibonacci number %d: %d' % (n, result))
57
58 # Send response back
59 flag = rpc.send(np.int32(result))
60 if not flag:
61 raise RuntimeError('server(P): ERROR sending')
62
63 print('Goodbye from Python server')
64
65
66if __name__ == '__main__':
67 main()
1import sys
2import numpy as np
3from yggdrasil.interface.YggInterface import (
4 YggRpcClient, YggOutput)
5
6
7def main(iterations):
8 r"""Function to execute client communication with server that computes
9 numbers in the Fibonacci sequence.
10
11 Args:
12 iterations (int): The number of Fibonacci numbers to log.
13
14 """
15
16 print('Hello from Python client: iterations = %d ' % iterations)
17
18 # Set up connections matching yaml
19 # RPC client-side connection will be $(server_name)_$(client_name)
20 rpc = YggRpcClient("server_client", "%d", "%d")
21 log = YggOutput("output_log", 'fib(%-2d) = %-2d\n')
22
23 # Iterate over Fibonacci sequence
24 for i in range(1, iterations + 1):
25
26 # Call the server and receive response
27 print('client(Python): Calling fib(%d)' % i)
28 ret, result = rpc.call(np.int32(i))
29 if not ret:
30 raise RuntimeError('client(Python): RPC CALL ERROR')
31 fib = result[0]
32 print('client(Python): Response fib(%d) = %d' % (i, fib))
33
34 # Log result by sending it to the log connection
35 ret = log.send(np.int32(i), fib)
36 if not ret:
37 raise RuntimeError('client(Python): SEND ERROR')
38
39 print('Goodbye from Python client')
40
41
42if __name__ == '__main__':
43 # Take number of iterations from the first argument
44 main(int(sys.argv[1]))
The interface server-side API call (YggRpcServer for Python),
requires 3 input variables: the name of the server channel (this will be
the name of the server model), a format string for input to the server model,
and a format string for output from the server model. The client-side API call
(YggRpcClient for Python), also requires 3 input variables: the name of the
client channel (this is the name of the server model joined with the name of
the client model by an underscore, <server>_<client>
, a format string for
input to the server model, and a format string for output form the server
model. The last two arguments (the format strings) to both the server and
client API calls should be the same.
In the server model YAML, the key/value pair is_server: True
needs
to be added to the model entry to indicate that the model will be called
as a server and requires a set of RPC channels. In the client model YAML,
the key/value pair client_of: <server_model_name>
is required to indicate
that the model will act as a client of the <server_model_name>
model.
Model YAML:
1---
2
3model:
4 name: server
5 language: python
6 args: ./src/server.py
7 is_server: True # Creates a RPC server queue called "server"
8 inputs:
9 name: params
10 default_file:
11 name: ./Input/server_params.txt
12 filetype: ascii
1---
2
3model:
4 name: client
5 language: python
6 args:
7 - ./src/client.py
8 - 3 # Pass the number of iterations that should be performed
9 client_of: server # Creates an RPC client queue "server_client"
10 outputs: output_log
11
12connections:
13 input: output_log
14 output: client_output.txt
15 in_temp: True
In addition to the RPC API call, the example server also has an input params
.
Models acting as servers can have as many inputs/outputs as desired in addition to
the RPC connections. While the example input is not used to modify the output
in this example, such a comm could be used to initialize a model with
parameters and/or initial conditions.
Using Existing Inputs/Outputs¶
Models that have already been integrated via yggdrasil can also be turned
into servers without modifying the code. Instead of passing a boolean to
the is_server
parameter, such models can provide a mapping with input
and output
parameters that explicitly outline which of a existing model’s
inputs/outputs should be used for the RPC call. Receive/send calls to named
input/output channels will then behave as receive/send calls on a server
interface comm.
Todo
Example source code and YAML of server replacing an existing input/output
One Server, Two Clients¶
There is no limit on the number of clients that can connect to a single
server. In the example below, the server is the same as above. The client
code is also essentially the same except that it has been modified to take
a client_index
variable that provides information to differentiates
between two clients using the same source code.
Model Code:
1import numpy as np
2from yggdrasil.interface.YggInterface import YggInput, YggRpcServer
3
4
5def get_fibonacci(n):
6 r"""Compute the nth number of the Fibonacci sequence.
7
8 Args:
9 n (int): Index of the Fibonacci number in the Fibonacci sequence that
10 should be returned.
11
12 Returns:
13 int: The nth Fibonacci number.
14
15 """
16 pprev = 0
17 prev = 1
18 result = 1
19 fib_no = 1
20 while fib_no < n:
21 result = prev + pprev
22 pprev = prev
23 prev = result
24 fib_no = fib_no + 1
25 return result
26
27
28def main():
29 r"""Function to execute server communication and computation in a loop."""
30
31 print('Hello from Python server!')
32
33 # Get parameters
34 inp = YggInput("params")
35 retval, params = inp.recv()
36 if not retval:
37 raise RuntimeError('server: ERROR receiving parameters')
38 print('server: Parameters = %s' % params)
39
40 # Create server-side rpc conneciton using model name
41 rpc = YggRpcServer("server", "%d", "%d")
42
43 # Continue receiving requests until the connection is closed when all
44 # clients have disconnected.
45 while True:
46 print('server: receiving...')
47 retval, rpc_in = rpc.recv()
48 if not retval:
49 print('server: end of input')
50 break
51
52 # Compute fibonacci number
53 n = rpc_in[0]
54 print('server: Received request for Fibonacci number %d' % n)
55 result = get_fibonacci(n)
56 print('server: Sending response for Fibonacci number %d: %d' % (n, result))
57
58 # Send response back
59 flag = rpc.send(np.int32(result))
60 if not flag:
61 raise RuntimeError('server: ERROR sending')
62
63 print('Goodbye from Python server')
64
65
66if __name__ == '__main__':
67 main()
1import sys
2import numpy as np
3from yggdrasil.interface.YggInterface import (
4 YggRpcClient, YggOutput)
5
6
7def main(iterations, client_index):
8 r"""Function to execute client communication with server that computes
9 numbers in the Fibonacci sequence.
10
11 Args:
12 iterations (int): The number of Fibonacci numbers to log.
13 client_index (int): Index of the client in total list of clients.
14
15 """
16
17 print('Hello from Python client%d: iterations = %d ' % (client_index,
18 iterations))
19
20 # Set up connections matching yaml
21 # RPC client-side connection will be $(server_name)_$(client_name)
22 rpc = YggRpcClient("server_client%d" % client_index, "%d", "%d")
23 log = YggOutput("output_log%d" % client_index, 'fib(%-2d) = %-2d\n')
24
25 # Iterate over Fibonacci sequence
26 for i in range(1, iterations + 1):
27
28 # Call the server and receive response
29 print('client%d(Python): Calling fib(%d)' % (client_index, i))
30 ret, result = rpc.call(np.int32(i))
31 if not ret:
32 raise RuntimeError('client%d(Python): RPC CALL ERROR' % client_index)
33 fib = result[0]
34 print('client%d(Python): Response fib(%d) = %d' % (client_index, i, fib))
35
36 # Log result by sending it to the log connection
37 ret = log.send(np.int32(i), fib)
38 if not ret:
39 raise RuntimeError('client%d(Python): SEND ERROR' % client_index)
40
41 print('Goodbye from Python client%d' % client_index)
42
43
44if __name__ == '__main__':
45 # Take number of iterations from the first argument and the
46 # client index from the second
47 main(int(sys.argv[1]), int(sys.argv[2]))
The server YAML is the same as above. The client YAML now has entries for two models which are both clients of the server model and call the same source code.
Model YAML:
1---
2
3model:
4 name: server
5 language: python
6 args: ./src/server.py
7 is_server: True # Creates a RPC server queue called "server"
8 inputs:
9 name: params
10 default_file:
11 name: ./Input/server_params.txt
12 filetype: ascii
1---
2
3models:
4 - name: client1
5 language: python
6 args:
7 - ./src/client.py
8 - 3 # Pass the number of iterations that should be performed
9 - 1 # Pass index of the client
10 client_of: server # Creates an RPC client queue "server_client"
11 outputs: output_log1
12 - name: client2
13 language: python
14 args:
15 - ./src/client.py
16 - 5 # Pass the number of iterations that should be performed
17 - 2 # Pass index of the client
18 client_of: server # Creates an RPC client queue "server_client"
19 outputs: output_log2
20
21connections:
22 - input: output_log1
23 output: client_output1.txt
24 in_temp: true
25 - input: output_log2
26 output: client_output2.txt
27 in_temp: true
During runtime, request messages from both clients will be routed to the server model which will process the requests in the order they are received.
Two Servers, Two Clients¶
There is also no limit on the number of copies of a server model that can be
used to responsd to RPC requests from the clients. In the example below, the
server and clients are the same as above, but 2 copies of the server model
are run as specified by the model copies
parameter in the server YAML.
Model YAML:
1---
2
3model:
4 name: server
5 language: python
6 args: ./src/server.py
7 is_server: True # Creates a RPC server queue called "server"
8 copies: 2
9 inputs:
10 name: params
11 default_file:
12 name: ./Input/server_params.txt
13 filetype: ascii
1---
2
3models:
4 - name: client1
5 language: python
6 args:
7 - ./src/client.py
8 - 3 # Pass the number of iterations that should be performed
9 - 1 # Pass index of the client
10 client_of: server # Creates an RPC client queue "server_client"
11 outputs: output_log1
12 - name: client2
13 language: python
14 args:
15 - ./src/client.py
16 - 5 # Pass the number of iterations that should be performed
17 - 2 # Pass index of the client
18 client_of: server # Creates an RPC client queue "server_client"
19 outputs: output_log2
20
21connections:
22 - input: output_log1
23 output: client_output1.txt
24 in_temp: true
25 - input: output_log2
26 output: client_output2.txt
27 in_temp: true
This allow client requests to be returned twice as fast, but precludes any use of an internal state by the server model as there is no way for a client to be sure that the same server model is responding to its requests and only its requests.
Wrapped Function Server¶
Models that are created by letting yggdrasil automatically wrap a function can also act as servers and/or clients. In the example below, the model acting as a server is a very simple function that takes a string as an input and returns the same string and the client is a function that takes a string as an input, calls the server models with the input string and returns the response.
When a client model is autowrapped from a function, additional care must be
taken so that the client RPC comm can be reused during each call to the
model. In interpreted models (Python, R, MATLAB), this is done by passing the
keyword global_scope
to the RPC client interface initialization function
(YggRpcClient
in Python). In compiled models (C, C++, Fortran), this is
done by framing RPC client interface initialization calls with the
WITH_GLOBAL_SCOPE
macro (see the language specific versions of this
example for specifics).
Model Code:
1def model_function(in_buf):
2 print("server(Python): %s" % in_buf)
3 out_buf = in_buf
4 return out_buf
1from yggdrasil.languages.Python.YggInterface import YggRpcClient
2
3
4def model_function(in_buf):
5 # The global_scope keyword is required to ensure that the comm persists
6 # between function calls
7 rpc = YggRpcClient('server_client', global_scope=True)
8 print("client(Python): %s" % in_buf)
9 ret, result = rpc.call(in_buf)
10 if not ret:
11 raise RuntimeError('client(Python): RPC CALL ERROR')
12 out_buf = result
13 return out_buf
The RPC connection between the server and the client is controlled by the
same is_server
and client_of
YAML parameters as before.
Model YAML:
1model:
2 name: server
3 language: python
4 args: ./src/server.py
5 function: model_function
6 is_server: True
1model:
2 name: client
3 language: python
4 args: ./src/client.py
5 function: model_function
6 client_of: server
7 inputs:
8 name: in_buf
9 default_file:
10 name: ./Input/input.txt
11 filetype: ascii
12 outputs:
13 name: out_buf
14 default_file:
15 name: ./client_output.txt
16 in_temp: true
By default, all inputs to a wrapped server function will be used in
the RPC call. However if only some of the inputs should be passed in by the
RPC calls, they can be specified explicitly by providing the is_server
parameter with a map that contains input
and output
parameters that
map to the names of function input/output variables (as in the case of
using existing input/output channels above).