TaskParallelScalablePipeline

# Task-parallel Scalable Pipeline

Include the Header
TaskParallelScalablePipeline_1IncludeTheScalablePipelineHeader
Create a Scalable Pipeline Module Task
TaskParallelScalablePipeline_1CreateAScalablePipelineModuleTask
Reset a Placeholder Scalable Pipeline
TaskParallelScalablePipeline_1ResetAPlaceholderScalablePipeline
Use Other Iterator Types
TaskParallelScalablePipeline_1ScalablePipelineUseOtherIteratorTypes
Learn More about Taskflow Pipeline
TaskParallelScalablePipeline_1ParallelScalablePipelineLearnMore

Unlike tf::Pipeline (see Task-parallel Pipeline) that instantiates all pipes at the construction time, Taskflow provides a scalable alternative called tf::ScalablePipeline to allow variable assignments of pipes using range iterators. A scalable pipeline is thus more flexible for applications to create a pipeline scheduling framework whose pipeline structure depends on runtime variables.

# Include the Header

You need to include the header file, `taskflow/algorithm/pipeline.hpp`, for creating a scalable pipeline scheduling framework.

    #include<taskflow/algorithm/pipeline.hpp>

# Create a Scalable Pipeline Module Task

Similar to tf::Pipeline, tf::ScalablePipeline is a composable graph object to implement a pipeline scheduling framework in a taskflow. The key difference between tf::Pipeline and tf::ScalablePipeline is that a scalable pipeline can accept variable assignments of pipes rather than instantiating all pipes at construction or programming time. Users define a linear range of pipes, each of the same callable type, and apply that range to construct a scalable pipeline. Between successive runs, users can reset the pipeline to a different range of pipes. The following code creates a scalable pipeline that uses four parallel lines to schedule tokens through three serial pipes in the given vector, then resetting that pipeline to a new range of five serial pipes:

    tf::Taskflowtaskflow("pipeline");
    tf::Executorexecutor;

    constsize_tnum_lines=4;

    //createdatastorage
    std::array<int, num_lines>buffer;

    //definethepipecallable
    autopipe_callable=[&buffer](tf::Pipeflow&pf)mutable{
    switch(pf.pipe()){
    //firststagegeneratesonly5schedulingtokensandsavesthe
    //tokennumberintothebuffer.
    case0:{
    if(pf.token()==5){
    pf.stop();
    }
    else{
    printf("stage1:inputtoken=%zu\n",pf.token());
    buffer[pf.line()]=pf.token();
    }
    return;
    }
    break;

    //otherstagespropagatethepreviousresulttothispipeand
    //incrementitbyone
    default:{
    printf(
    "stage%zu:inputbuffer[%zu]=%d\n",pf.pipe(),pf.line(),buffer[pf.line()]
    );
    buffer[pf.line()]=buffer[pf.line()]+1;
    }
    break;
    }
    };

    //createavectorofthreepipes
    std::vector<tf::Pipe<std::function<void(tf::Pipeflow&)>>>pipes;

    for(size_ti=0;i<3;i++){
    pipes.emplace_back(tf::PipeType::SERIAL,pipe_callable);
    }

    //createapipelineoffourparallellinesbasedonthegivenvectorofpipes
    tf::ScalablePipelinepl(num_lines,pipes.begin(),pipes.end());

    //buildthepipelinegraphusingcomposition
    tf::Taskinit=taskflow.emplace([](){std::cout<<"ready\n";})
    .name("startingpipeline");
    tf::Tasktask=taskflow.composed_of(pl)
    .name("pipeline");
    tf::Taskstop=taskflow.emplace([](){std::cout<<"stopped\n";})
    .name("pipelinestopped");

    //createtaskdependency
    init.precede(task);
    task.precede(stop);

    //dumpthepipelinegraphstructure(withcomposition)
    taskflow.dump(std::cout);

    //runthepipeline
    executor.run(taskflow).wait();

    //resetthepipelinetoanewrangeoffivepipesandstartsfrom
    //theinitialstate(i.e.,tokencountsfromzero)
    for(size_ti=0;i<2;i++){
    pipes.emplace_back(tf::PipeType::SERIAL,pipe_callable);
    }
    pl.reset(pipes.begin(),pipes.end());

    executor.run(taskflow).wait();

The program defines a uniform pipe type of `tf::Pipe<std::function<void(tf::Pipeflow&)>>` and keep all pipes in a vector that is amenable to change. Then, it constructs a scalable pipeline using two range iterators, `[first, last)`, that point to the beginning and the end of the pipe vector, resulting in a pipeline of three serial stages:

Then, the program appends another two pipes into the vector and resets the pipeline to the new range of two additional pipes, resulting in a pipeline of five serial stages:

When resetting a scalable pipeline to a new range, it will start from the initial state as if it has just been constructed, i.e., the token number counts from zero.

Unlike tf::Pipeline that keeps the given pipes in a std::tuple object, tf::ScalablePipeline does not own the given pipe but maintains a vector of iterators to each pipe in the given range. It is your responsibility to keep those pipe objects alive during the execution of the pipeline task.

# Reset a Placeholder Scalable Pipeline

It is possible to create a scalable pipeline as a placeholder using the constructor tf::ScalablePipeline(size_t num_lines) and reset it to another range later in the application. The following code creates a task to emplace a range of pipes and reset the pipeline to that range, before running the pipeline task:

    tf::Executorexecutor;
    tf::Taskflowtaskflow;

    size_tnum_pipes=10;
    size_tnum_lines=10;

    std::vector<tf::Pipe<std::function<void(tf::Pipeflow&)>>>pipes;
    tf::ScalablePipeline<typenamedecltype(pipes)::iterator>spl(num_lines);

    tf::Taskinit=taskflow.emplace([&](){
    for(size_ti=0;i<num_pipes;i++){
    pipes.emplace_back(tf::PipeType::SERIAL,[&](tf::Pipeflow&pf){
    if(pf.pipe()==0&&pf.token()==1024){
    pf.stop();
    return;
    }
    });
    }
    spl.reset(pipes.begin(),pipes.end());
    }).name("init");

    tf::Taskpipeline=taskflow.composed_of(spl).name("pipeline");
    pipeline.succeed(init);
    executor.run(taskflow).wait();

The task graph of this program is shown below:

It is your responsibility to ensure a scalable pipeline has a valid structure before running it. A valid pipeline must have at least one parallel line and one pipe, where the first pipe is a serial type.

Similarly, you can create an empty scalable pipeline using the default constructor tf::ScalablePipeline() and reset it later in your program.

    std::vector<tf::Pipe<std::function<void(tf::Pipeflow&)>>>pipes;
    tf::ScalablePipeline<typenamedecltype(pipes)::iterator>spl;
    //createpipes...
    spl.reset(num_lines,pipes.begin(),pipes.end());

# Use Other Iterator Types

When assigning a range to a scalable pipeline, the pipeline fetches all pipe iterators in that range to an internal vector. This organization allows invoking a pipe callable to be a random accessible operation, regardless of the pipe container type. Taskflow does not have much restriction on the iterator type, as long as these pipes can be iterated in a sequential order using the postfix increment operator, `++`.

    //usevectortostorepipes
    std::vector<tf::Pipe<std::function<void(tf::Pipeflow&)>>>vector;
    tf::ScalablePipelinespl1(num_lines,vector.begin(),vector.end());

    //uselisttostorepipes
    std::list<tf::Pipe<std::function<void(tf::Pipeflow&)>>>list;
    tf::ScalablePipelinespl2(num_lines,list.begin(),list.end());

# Learn More about Taskflow Pipeline

Visit the following pages to learn more about pipeline:

- Task-parallel Pipeline

- Data-parallel Pipeline

- Text Processing Pipeline

- Graph Processing Pipeline

- Taskflow Processing Pipeline