Creating a Data Flow Graph (Including Kernels) - 2023.2 English

AI Engine Kernel and Graph Programming Guide (UG1079)

Document ID
UG1079
Release Date
2023-12-04
Version
2023.2 English
This following process describes how to construct data flow graphs in C++.
  1. Define your application graph class in a separate header file (for example project.h). First, add the Adaptive Data Flow (ADF) header (adf.h) and include the kernel function prototypes. The ADF library includes all the required constructs for defining and executing the graphs on AI Engines.
    #include <adf.h>
    #include "kernels.h"
  2. Define your graph class by using the objects which are defined in the adf namespace. All user graphs are derived from the class graph.
    include <adf.h>
    #include "kernels.h"
    
    using namespace adf;
    
    class simpleGraph : public graph {
    private:
      kernel first;
      kernel second;
    };

    This is the beginning of a graph class definition that declares two kernels (first and second).

  3. Add some top-level input/output objects, input_plio and output_plio, to the graph.
    #include <adf.h>
    #include "kernels.h"
    
    using namespace adf;
    
    class simpleGraph : public graph {
    private:
      kernel first;
      kernel second;
    public:
      input_plio in;
      output_plio out;
    
    };
  4. Use the kernel::create function to instantiate the first and second C++ kernel objects using the functionality of the C function simple.
    #include <adf.h>
    #include "kernels.h"
    
    using namespace adf;
    
    class simpleGraph : public graph {
    private:
      kernel first;
      kernel second;
    public:
      input_plio in;
      output_plio out;
      simpleGraph() {
          first = kernel::create(simple);
          second = kernel::create(simple);
      }
    };
  5. Configure input/output objects with specified PLIO width and input/output files, and add the connectivity information, which is equivalent to the nets in a data flow graph. In this description, input/output objects are referenced by indices. The first input buffer or stream argument in the simple function is assigned index 0 in an array of input ports (in). Subsequent input arguments take ascending consecutive indices. The first output buffer or stream argument in the simple function is assigned index 0 in an array of output ports (out). Subsequent output arguments take ascending consecutive indices.
    #include <adf.h>
    #include "kernels.h"
    
    using namespace adf;
    
    class simpleGraph : public graph {
    private:
      kernel first;
      kernel second;
    public:
      input_plio in;
      output_plio out;
    
      simpleGraph() {
        first = kernel::create(simple);
        second = kernel::create(simple);
    
        in = input_plio::create(plio_32_bits, "data/input.txt");
        out = output_plio::create(plio_32_bits, "data/output.txt");
        connect(in.out[0], first.in[0]);
        connect(first.out[0], second.in[0]);
        connect(second.out[0], out.in[0]);
        dimensions(first.in[0]) = {128};
        dimensions(first.out[0]) = {128};
        dimensions(second.in[0]) = {128};
        dimensions(second.out[0]) = {128};
      }
    };


    This figure represents the graph connectivity specified in the previous graph code. Graph connectivity can be viewed when you open the compilation results in the Vitis IDE. For more information, see Viewing Compilation Results in the AI Engine Tools and Flows User Guide (UG1076). As shown in the previous figure, the input port from the top level is connected into the input port of the first kernel, the output port of the first kernel is connected to the input port of the second kernel, and the output port of the second kernel is connected to the output exposed to the top level. The first kernel executes when 128 bytes of data (32 complex samples) are collected in a buffer from an external source. This is specified using a dimensions(first.in[0])={128} construct. Likewise, the second kernel executes when its input buffer has valid data being produced as the output of the first kernel. Finally, the output of the second kernel is connected to the top-level output port and the dimensions(second.out[0])={128} specifies the number of bytes of data kernel will produce upon termination.

    buf0 and buf0d are ping pong buffers allocated for the first kernel input buffer. Similarly, buf2 and buf2d are ping pong buffers allocated for the second kernel output buffer. Notice that buf1 which is the buffer output from the first kernel to the second kernel is not a ping pong buffer. This is because both the first and second kernel buffers are placed in a single AI Engine tile where they will execute sequentially. See Run-Time Ratio for more information.

  6. Set the source file and tile usage for each of the kernels. The source file kernel.cc contains kernel first and kernel second source code. Then the ratio of the function run time compared to the cycle budget, known as the run-time ratio, and must be between 0 and 1. The cycle budget is the number of instruction cycles a function can take to either consume data from its input (when dealing with a rate limited input data stream), or to produce a block of data on its output (when dealing with a rate limited output data stream). This cycle budget can be affected by changing the block sizes.
    #include <adf.h>
    #include "kernels.h"
    
    using namespace adf;
    
    class simpleGraph : public graph {
    private:
      kernel first;
      kernel second;
    public:
      input_plio in;
      output_plio out;
    
      simpleGraph(){
    
        first = kernel::create(simple);
        second = kernel::create(simple);
    
        in = input_plio::create(plio_32_bits, "data/input.txt");
        out = output_plio::create(plio_32_bits, "data/output.txt");
            
        connect(in.out[0], first.in[0]);
        connect(first.out[0], second.in[0]);
        connect(second.out[0], out.in[0]);
        dimensions(first.in[0]) = {128};
        dimensions(first.out[0]) = {128};
        dimensions(second.in[0]) = {128};
        dimensions(second.out[0]) = {128};
    
        source(first) = "kernels.cc";
        source(second) = "kernels.cc";
    
        runtime<ratio>(first) = 0.1;
        runtime<ratio>(second) = 0.1;
    
      }
    };
    Note: See Run-Time Ratio for more information.
  7. Define a top-level application file (for example project.cpp) that contains an instance of your graph class.
    #include "project.h"
    
    simpleGraph mygraph;
    
    int main(void) {
      adf::return_code ret;
      mygraph.init();
      ret=mygraph.run(<number_of_iterations>);
      if(ret!=adf::ok){
        printf("Run failed\n");
        return ret;
      }
      ret=mygraph.end();
      if(ret!=adf::ok){
        printf("End failed\n");
        return ret;
      }
      return 0; //Must have return statement
    }
Important: By default, the mygraph.run() option specifies a graph that runs forever. The aiecompiler generates code to execute the data flow graph in a perpetual while loop. To limit the execution of the graph for debugging and test, specify the mygraph.run(<number_of_iterations>) in the graph code. The specified number of iterations can be one or more.
Important: The main function must have a return statement. Otherwise, aiecompiler will error out.

ADF APIs have return enumerate type return_code to show the API running status.

The main program is the driver for the graph. It is used to load, execute, and terminate the graph.

Note: Kernel code must be written in such a way that no name clashes occur when two kernels get assigned to the same core.