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 = "../../common/mnist_data/mnist_cnn.pt"
 39  character(len=*), parameter :: script_key = "mnist_script"
 40  character(len=*), parameter :: script_file = "../../common/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