Fortran#
In this section, examples are presented using the SmartRedis Fortran
API to interact with the RedisAI tensor, model, and script
data types.  Additionally, an example of utilizing the
SmartRedis DataSet API is also provided.
Note
The Fortran API examples rely on the SSDB environment
variable being set to the address and port of the Redis database.
Note
The Fortran API examples are written
to connect to a clustered database or clustered SmartSim Orchestrator.
Update the Client constructor cluster flag to .false.
to connect to a single shard (single compute host) database.
Error handling#
The core of the SmartRedis library is written in C++ which utilizes the
exception handling features of the language to catch errors. This same
functionality does not exist in Fortran, so instead most SmartRedis
methods are functions that return error codes that can be checked. This
also has the added benefit that Fortran programs can incorporate
SmartRedis calls within their own error handling methods. A full list of
return codes for Fortran can be found in enum_fortran.inc. Additionally, the
errors module has get_last_error and print_last_error to retrieve
the text of the error message emitted within the C++ code.
Tensors#
The SmartRedis Fortran client is used to communicate between a Fortran client and the Redis database. In this example, the client will be used to send an array to the database and then unpack the data into another Fortran array.
This example will go step-by-step through the program and then present the entirety of the example code at the end.
Importing and declaring the SmartRedis client
The SmartRedis client must be declared as the derived type
client_type imported from the smartredis_client module.
program example
  use smartredis_client, only : client_type
  type(client_type) :: client
end program example
Initializing the SmartRedis client
The SmartRedis client needs to be initialized before it can be used
to interact with the database. Within Fortran this is
done by calling the type-bound procedure
initialize with the input argument .true.
if using a clustered database or .false. otherwise.
program example
  use smartredis_client, only : client_type
  type(client_type) :: client
  integer :: return_code
  return_code = client%initialize(.false.) ! Change .false. to true if using a clustered database
  if (return_code .ne. SRNoError) stop 'Error in initializing client'
end program example
Putting a Fortran array into the database
After the SmartRedis client has been initialized,
a Fortran array of any dimension and shape
and with a type of either 8, 16, 32, 64 bit
integer or 32 or 64-bit real can be
put into the database using the type-bound
procedure put_tensor.
In this example, as a proxy for model-generated
data, the array send_array_real_64 will be
filled with random numbers and stored in the
database using put_tensor. This subroutine
requires the user to specify a string used as the
‘key’ (here: send_array) identifying the tensor
in the database, the array to be stored, and the
shape of the array.
1  call random_number(send_array_real_64)
2
3  ! Initialize a client
4  result = client%initialize("smartredis_put_get_3D")
5  if (result .ne. SRNoError) error stop 'client%initialize failed'
6
7  ! Send a tensor to the database via the client and verify that we can retrieve it
8  result = client%put_tensor("send_array", send_array_real_64, shape(send_array_real_64))
9  if (result .ne. SRNoError) error stop 'client%put_tensor failed'
Unpacking an array stored in the database
‘Unpacking’ an array in SmartRedis refers to filling
a Fortran array with the values of a tensor
stored in the database.  The dimensions and type of
data of the incoming array and the pre-declared
array are checked within the client to
ensure that they match. Unpacking requires
declaring an array and using the unpack_tensor
procedure.  This example generates an array
of random numbers, puts that into the database,
and retrieves the values from the database
into a different array.
 1! BSD 2-Clause License
 2!
 3! Copyright (c) 2021-2024, Hewlett Packard Enterprise
 4! All rights reserved.
 5!
 6! Redistribution and use in source and binary forms, with or without
 7! modification, are permitted provided that the following conditions are met:
 8!
 9! 1. Redistributions of source code must retain the above copyright notice, this
10!    list of conditions and the following disclaimer.
11!
12! 2. Redistributions in binary form must reproduce the above copyright notice,
13!    this list of conditions and the following disclaimer in the documentation
14!    and/or other materials provided with the distribution.
15!
16! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27program main
28
29  use iso_c_binding
30  use smartredis_client, only : client_type
31
32  implicit none
33
34#include "enum_fortran.inc"
35
36  integer, parameter :: dim1 = 10
37  integer, parameter :: dim2 = 20
38  integer, parameter :: dim3 = 30
39
40  real(kind=8),    dimension(dim1, dim2, dim3) :: recv_array_real_64
41  real(kind=c_double),    dimension(dim1, dim2, dim3) :: send_array_real_64
42
43  integer :: i, j, k, result
44  type(client_type) :: client
45
46  call random_number(send_array_real_64)
47
48  ! Initialize a client
49  result = client%initialize("smartredis_put_get_3D")
50  if (result .ne. SRNoError) error stop 'client%initialize failed'
51
52  ! Send a tensor to the database via the client and verify that we can retrieve it
53  result = client%put_tensor("send_array", send_array_real_64, shape(send_array_real_64))
54  if (result .ne. SRNoError) error stop 'client%put_tensor failed'
55  result = client%unpack_tensor("send_array", recv_array_real_64, shape(recv_array_real_64))
56  if (result .ne. SRNoError) error stop 'client%unpack_tensor failed'
57
58  ! Done
59  call exit()
60
61end program main
Datasets#
The following code snippet shows how to use the Fortran Client to store and retrieve dataset tensors and dataset metadata scalars.
 1! BSD 2-Clause License
 2!
 3! Copyright (c) 2021-2024, Hewlett Packard Enterprise
 4! All rights reserved.
 5!
 6! Redistribution and use in source and binary forms, with or without
 7! modification, are permitted provided that the following conditions are met:
 8!
 9! 1. Redistributions of source code must retain the above copyright notice, this
10!    list of conditions and the following disclaimer.
11!
12! 2. Redistributions in binary form must reproduce the above copyright notice,
13!    this list of conditions and the following disclaimer in the documentation
14!    and/or other materials provided with the distribution.
15!
16! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27program main
28
29  use iso_c_binding
30  use smartredis_dataset, only : dataset_type
31  use smartredis_client, only : client_type
32
33  implicit none
34
35#include "enum_fortran.inc"
36
37  integer, parameter :: dim1 = 10
38  integer, parameter :: dim2 = 20
39  integer, parameter :: dim3 = 30
40
41  real(kind=c_float),      dimension(dim1, dim2, dim3) :: recv_array_real_32
42
43  real(kind=c_float),      dimension(dim1, dim2, dim3) :: true_array_real_32
44
45  character(len=16) :: meta_float = 'meta_float'
46
47  real(kind=c_float),  dimension(dim1) :: meta_flt_vec
48  real(kind=c_float), dimension(:), pointer :: meta_flt_recv
49
50  integer :: i, result
51  type(dataset_type) :: dataset
52  type(client_type) :: client
53
54  ! Fill array
55  call random_number(true_array_real_32)
56
57  ! Initialize a dataset
58  result = dataset%initialize("example_fortran_dataset")
59  if (result .ne. SRNoError) error stop 'dataset initialization failed'
60
61  ! Add a tensor to the dataset and verify that we can retrieve it
62  result = dataset%add_tensor("true_array_real_32", true_array_real_32, shape(true_array_real_32))
63  if (result .ne. SRNoError) error stop 'dataset%add_tensor failed'
64  result = dataset%unpack_dataset_tensor("true_array_real_32", recv_array_real_32, shape(recv_array_real_32))
65  if (result .ne. SRNoError) error stop 'dataset%unpack_dataset_tensor failed'
66
67  call random_number(meta_flt_vec)
68
69  ! Add metascalars to the dataset and verify that we can retrieve them
70  do i=1,dim1
71    result = dataset%add_meta_scalar(meta_float, meta_flt_vec(i))
72    if (result .ne. SRNoError) error stop 'dataset%add_meta_scalar failed'
73  enddo
74  result = dataset%get_meta_scalars(meta_float, meta_flt_recv)
75  if (result .ne. SRNoError) error stop 'dataset%get_meta_scalars failed'
76
77  ! Initialize a client
78  result = client%initialize("smartredis_dataset")
79  if (result .ne. SRNoError) error stop 'client%initialize failed'
80
81  ! Send the dataset to the database via the client
82  result = client%put_dataset(dataset)
83  if (result .ne. SRNoError) error stop 'client%put_dataset failed'
84
85  ! Done
86  call exit()
87
88end program main
Models#
For an example of placing a model in the database
and executing the model using a stored tensor,
see the Parallel (MPI) execution example.  The
aforementioned example is customized to show how
key collisions can be avoided in parallel
applications, but the Client API calls
pertaining to model actions are identical
to non-parallel applications.
Scripts#
For an example of placing a PyTorch script in the database
and executing the script using a stored tensor,
see the Parallel (MPI) execution example.  The
aforementioned example is customized to show how
key collisions can be avoided in parallel
applications, but the Client API calls
pertaining to script actions are identical
to non-parallel applications.
Parallel (MPI) execution#
In this example, an MPI program that
sets a model, sets a script, executes a script,
executes a model, sends a tensor, and receives
a tensor is shown.  This example illustrates
how keys can be prefixed to prevent key
collisions across MPI ranks.  Note that only one
model and script are set, which is shared across
all ranks.  It is important to note that the
Client API calls made in this program are
equally applicable to non-MPI programs.
This example will go step-by-step through the program and then present the entirety of the example code at the end.
The MNIST dataset and model typically take images of digits and quantifies how likely that number is to be 0, 1, 2, etc.. For simplicity here, this example instead generates random numbers to represent an image.
Initialization
At the top of the program, the SmartRedis Fortran client (which is coded as a Fortran module) is imported using
use smartredis_client, only : client_type
where client_type is a Fortran derived-type containing
the methods used to communicate with the RedisAI database.
A particular instance is declared via
type(client_type) :: client
An initializer routine, implemented as a type-bound procedure, must be called before any of the other methods are used:
return_code = client%initialize(.true.)
if (return_code .ne. SRNoError) stop 'Error in initializing client'
The only optional argument to the initialize
routine is to determine whether the RedisAI
database is clustered (i.e. spread over a number
of nodes, .true.) or exists as a single instance.
If an individual rank is expected to send only its local data, a separate client must be initialized on every MPI task Furthermore, to avoid the collision of key names when running on multiple MPI tasks, we store the rank of the MPI process which will be used as the suffix for all keys in this example.
On the root MPI task, two additional client methods
(set_model_from_file and set_script_from_file)
are called. set_model_from_file loads a saved
PyTorch model and stores it in the database using the key
mnist_model. Similarly, set_script_from_file
loads a script that can be used to process data on the
database cluster.
if (pe_id == 0) then
  return_code = client%set_model_from_file(model_key, model_file, "TORCH", "CPU")
  if (return_code .ne. SRNoError) stop 'Error in setting model'
  return_code = client%set_script_from_file(script_key, "CPU", script_file)
  if (return_code .ne. SRNoError) stop 'Error in setting script'
endif
This only needs to be done on the root MPI task because this example assumes that every rank is using the same model. If the model is intended to be rank-specific, a unique identifier (like the MPI rank) must be used.
At this point the initialization of the program is complete: each rank has its own SmartRedis client, initialized a PyTorch model has been loaded and stored into the database with its own identifying key, and a preprocessing script has also been loaded and stored in the database
Performing inference on Fortran data
The run_mnist subroutine coordinates the inference
cycle of generating data (i.e. the synthetic MNIST image) from
the application and then the use of the client to
run a preprocessing script on data within the database and to
perform an inference from the AI model. The local
variables are declared at the top of the subroutine and are
instructive to communicate the expected shape of the
inputs to the various client methods.
integer, parameter :: mnist_dim1 = 28
integer, parameter :: mnist_dim2 = 28
integer, parameter :: result_dim1 = 10
The first two integers mnist_dim1 and mnist_dim2
specify the shape of the input data. In the case of the
MNIST dataset, it expects a 4D tensor describing a ‘picture’
of a number with dimensions [1,1,28,28] representing a
batch size (of one) and a three dimensional array. result_dim1
specifies what the size of the resulting inference
will be. In this case, it is a vector of length 10, where
each element represents the probability that the data
represents a number from 0-9.
The next declaration declares the strings that will be used to define objects representing inputs/outputs from the scripts and inference models.
character(len=255) :: in_key
character(len=255) :: script_out_key
character(len=255) :: out_key
Note that these are standard Fortran strings. However, because the model and scripts may require the use of multiple inputs/outputs, these will need to be converted into a vector of strings.
character(len=255), dimension(1) :: inputs
character(len=255), dimension(1) :: outputs
In this case, only one input and output are expected the
vector of strings only need to be one element long. In the
case of multiple inputs/outputs, change the dimension
attribute of the inputs and outputs accordingly,
e.g. for two inputs this code would be character(len=255),
dimension(2) :: inputs.
Next, the input and output keys for the model and script are now constructed
in_key = "mnist_input_rank"//trim(key_suffix)
script_out_key = "mnist_processed_input_rank"//trim(key_suffix)
out_key = "mnist_processed_input_rank"//trim(key_suffix)
As mentioned previously, unique identifying keys are constructed by including a suffix based on MPI tasks.
The subroutine, in place of an actual simulation, next generates an array of random numbers and puts this array into the Redis database.
call random_number(array)
return_code = client%put_tensor(in_key, array, shape(array))
if (return_code .ne. SRNoError) stop 'Error putting tensor in the database'
The Redis database can now be called to run preprocessing scripts on these data.
inputs(1) = in_key
outputs(1) = script_out_key
return_code = client%run_script(script_name, "pre_process", inputs, outputs)
if (return_code .ne. SRNoError) stop 'Error running script'
The call to client%run_script specifies the
key used to identify the script loaded during
initialization, pre_process is the name of
the function to run that is defined in that script,
and the inputs/outputs are the vector of
keys described previously. In this case, the call to
run_script will trigger the RedisAI database
to execute pre_process on the generated data
(stored using the key mnist_input_rank_XX where XX
represents the MPI rank) and storing the result of
pre_process in the database as
mnist_processed_input_rank_XX. One key aspect to
emphasize, is that the calculations are done within the
database, not on the application side and the results
are not immediately available to the application. The retrieval
of data from the database is demonstrated next.
The data have been processed and now we can run the
inference model. The setup of the inputs/outputs
is the same as before, with the exception that the
input to the inference model, is stored using the
key mnist_processed_input_rank_XX
and the output will stored using the same key.
inputs(1) = script_out_key
outputs(1) = out_key
return_code = client%run_model(model_name, inputs, outputs)
if (return_code .ne. SRNoError) stop 'Error running model'
As before the results of running the inference are
stored within the database and are not available to
the application immediately. However, we can ‘retrieve’
the tensor from the database by using the unpack_tensor method.
return_code = client%unpack_tensor(out_key, result, shape(result))
if (return_code .ne. SRNoError) stop 'Error retrieving the tensor'
The result array now contains the outcome of the inference.
It is a 10-element array representing the likelihood that the
‘image’ (generated using the random numbers) is one of the numbers
[0-9].
Key points
The script, models, and data used here represent the coordination of different software stacks (PyTorch, RedisAI, and Fortran) however the application code is all written in standard Fortran. Any operations that need to be done to communicate with the database and exchange data are opaque to the application.
Source Code
Fortran program:
  1! BSD 2-Clause License
  2!
  3! Copyright (c) 2021-2024, Hewlett Packard Enterprise
  4! All rights reserved.
  5!
  6! Redistribution and use in source and binary forms, with or without
  7! modification, are permitted provided that the following conditions are met:
  8!
  9! 1. Redistributions of source code must retain the above copyright notice, this
 10!    list of conditions and the following disclaimer.
 11!
 12! 2. Redistributions in binary form must reproduce the above copyright notice,
 13!    this list of conditions and the following disclaimer in the documentation
 14!    and/or other materials provided with the distribution.
 15!
 16! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 17! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 18! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 19! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 20! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 21! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 22! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 23! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 24! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 25! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 26
 27program mnist_example
 28
 29  use mpi
 30  use iso_c_binding
 31  use smartredis_client, only : client_type
 32
 33  implicit none
 34
 35#include "enum_fortran.inc"
 36
 37  character(len=*), parameter :: model_key = "mnist_model"
 38  character(len=*), parameter :: model_file = "mnist_data/mnist_cnn.pt"
 39  character(len=*), parameter :: script_key = "mnist_script"
 40  character(len=*), parameter :: script_file = "mnist_data/data_processing_script.txt"
 41
 42  type(client_type) :: client
 43  integer :: err_code, pe_id, result
 44  character(len=2) :: key_suffix
 45
 46  ! Initialize MPI and get the rank of the processor
 47  call MPI_init(err_code)
 48  call MPI_comm_rank( MPI_COMM_WORLD, pe_id, err_code)
 49
 50  ! Format the suffix for a key as a zero-padded version of the rank
 51  write(key_suffix, "(A,I1.1)") "_",pe_id
 52
 53  ! Initialize a client
 54  result = client%initialize("smartredis_mnist")
 55  if (result .ne. SRNoError) error stop 'client%initialize failed'
 56
 57  ! Set up model and script for the computation
 58  if (pe_id == 0) then
 59    result = client%set_model_from_file(model_key, model_file, "TORCH", "CPU")
 60    if (result .ne. SRNoError) error stop 'client%set_model_from_file failed'
 61    result = client%set_script_from_file(script_key, "CPU", script_file)
 62    if (result .ne. SRNoError) error stop 'client%set_script_from_file failed'
 63  endif
 64
 65  ! Get all PEs lined up
 66  call MPI_barrier(MPI_COMM_WORLD, err_code)
 67
 68  ! Run the main computation
 69  call run_mnist(client, key_suffix, model_key, script_key)
 70
 71  ! Shut down MPI
 72  call MPI_finalize(err_code)
 73
 74  ! Check final result
 75  if (pe_id == 0) then
 76    print *, "SmartRedis Fortran MPI MNIST example finished without errors."
 77  endif
 78
 79  ! Done
 80  call exit()
 81
 82contains
 83
 84subroutine run_mnist( client, key_suffix, model_name, script_name )
 85  type(client_type), intent(in) :: client
 86  character(len=*),  intent(in) :: key_suffix
 87  character(len=*),  intent(in) :: model_name
 88  character(len=*),  intent(in) :: script_name
 89
 90  integer, parameter :: mnist_dim1 = 28
 91  integer, parameter :: mnist_dim2 = 28
 92  integer, parameter :: result_dim1 = 10
 93
 94  real, dimension(1,1,mnist_dim1,mnist_dim2) :: array
 95  real, dimension(1,result_dim1) :: output_result
 96
 97  character(len=255) :: in_key
 98  character(len=255) :: script_out_key
 99  character(len=255) :: out_key
100
101  character(len=255), dimension(1) :: inputs
102  character(len=255), dimension(1) :: outputs
103
104  ! Construct the keys used for the specifiying inputs and outputs
105  in_key = "mnist_input_rank"//trim(key_suffix)
106  script_out_key = "mnist_processed_input_rank"//trim(key_suffix)
107  out_key = "mnist_processed_input_rank"//trim(key_suffix)
108
109  ! Generate some fake data for inference and send it to the database
110  call random_number(array)
111  result = client%put_tensor(in_key, array, shape(array))
112  if (result .ne. SRNoError) error stop 'client%put_tensor failed'
113
114  ! Prepare the script inputs and outputs
115  inputs(1) = in_key
116  outputs(1) = script_out_key
117  result = client%run_script(script_name, "pre_process", inputs, outputs)
118  if (result .ne. SRNoError) error stop 'client%run_script failed'
119  inputs(1) = script_out_key
120  outputs(1) = out_key
121  result = client%run_model(model_name, inputs, outputs)
122  if (result .ne. SRNoError) error stop 'client%run_model failed'
123  output_result(:,:) = 0.
124  result = client%unpack_tensor(out_key, output_result, shape(output_result))
125  if (result .ne. SRNoError) error stop 'client%unpack_tensor failed'
126
127end subroutine run_mnist
128
129end program mnist_example
Python Pre-Processing:
1def pre_process(inp):
2    mean = torch.zeros(1).float().to(inp.device)
3    mean[0] = 2.0
4    temp = inp.float() * mean
5    return temp