Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,33 @@ jobs:
name: Build (CUDA 11.8)
runs-on: ubuntu-latest
container:
image: nvidia/cuda:11.8-devel-ubuntu22.04
image: nvidia/cuda:11.8.0-devel-ubuntu22.04
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install dependencies
run: |
apt-get update
apt-get install -y cmake
apt-get install -y cmake git

- name: Configure (minimal preset)
run: cmake --preset minimal -DMINI_IMAGE_PIPE_WITH_CVCUDA=OFF -DMINI_IMAGE_PIPE_WITH_TENSORRT=OFF -DMINI_IMAGE_PIPE_WITH_GSTREAMER=OFF
env:
CMAKE_CUDA_ARCHITECTURES: "80" # Single arch for faster CI build
- name: Configure
run: >
cmake -S . -B build-minimal
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_CUDA_ARCHITECTURES=80
-DMINI_IMAGE_PIPE_WITH_CVCUDA=OFF
-DMINI_IMAGE_PIPE_WITH_TENSORRT=OFF
-DMINI_IMAGE_PIPE_WITH_GSTREAMER=OFF

- name: Build
run: cmake --build --preset minimal
run: cmake --build build-minimal

- name: Test (CPU-only environment)
run: |
echo "Note: CUDA tests require GPU hardware and will be skipped on CPU-only runners."
echo "Running test executable to verify build integrity..."
./build/mini_image_pipe_tests --gtest_filter=* || echo "Tests require GPU - skipped"
ctest --test-dir build-minimal --output-on-failure || echo "Tests require GPU - skipped"
continue-on-error: true

gpu-validation:
Expand All @@ -89,18 +93,19 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Configure (minimal preset)
run: cmake --preset minimal
env:
CMAKE_CUDA_ARCHITECTURES: "80"
- name: Configure
run: >
cmake -S . -B build-minimal
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_CUDA_ARCHITECTURES=80

- name: Build tests and benchmark
run: |
cmake --build --preset minimal --target mini_image_pipe_tests
cmake --build --preset minimal --target benchmark_pipeline
cmake --build build-minimal --target mini_image_pipe_tests
cmake --build build-minimal --target benchmark_pipeline

- name: Run tests
run: ctest --preset minimal --output-on-failure
run: ctest --test-dir build-minimal --output-on-failure

- name: Run benchmark
run: ./build-minimal/benchmark_pipeline --iterations 5 --batch 2
2 changes: 1 addition & 1 deletion CMakePresets.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": 6,
"version": 3,
"configurePresets": [
{
"name": "default",
Expand Down
15 changes: 11 additions & 4 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,17 @@ for (int i = 0; i < 1000; i++) {

```cpp
// Process multiple frames in one call
std::vector<void*> inputs = {frame1, frame2, frame3};
std::vector<void*> outputs;

pipeline.executeBatch(inputs, outputs, width, height, channels);
std::vector<ImageBuffer> inputs = {
{frame1, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame2, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame3, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
};
std::vector<BatchOutput> outputs;

pipeline.executeBatch(inputs, outputs);
```

## Profiling
Expand Down
22 changes: 16 additions & 6 deletions docs/api/pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ public:
void* getOutput(int nodeId);

cudaError_t execute();
cudaError_t executeBatch(const std::vector<void*>& inputs,
std::vector<void*>& outputs,
int width, int height, int channels);
cudaError_t executeBatch(const std::vector<ImageBuffer>& inputs,
std::vector<BatchOutput>& outputs);

TaskGraph& getTaskGraph();
const TaskGraph& getTaskGraph() const;
Expand Down Expand Up @@ -91,10 +90,21 @@ void* out = pipeline.getOutput(n3);
### Batch execution

```cpp
std::vector<void*> inputs = {frame0, frame1, frame2};
std::vector<void*> outputs;
std::vector<ImageBuffer> inputs = {
{frame0, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame1, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame2, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
};
std::vector<BatchOutput> outputs;

cudaError_t err = pipeline.executeBatch(inputs, outputs, width, height, channels);
cudaError_t err = pipeline.executeBatch(inputs, outputs);
for (const auto& output : outputs) {
std::cout << "Sink node " << output.nodeId << " produced "
<< output.frames.size() << " frames" << std::endl;
}
```

## Error handling
Expand Down
10 changes: 6 additions & 4 deletions docs/blog/tutorials/video-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ cudaStreamDestroy(stream);
### Batch Processing

```cpp
std::vector<void*> frames;
std::vector<ImageBuffer> frames;
for (int i = 0; i < batchSize; i++) {
frames.push_back(getNextFrame());
frames.push_back({getNextFrame(), width, height, channels, width * channels,
sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false});
}

std::vector<void*> outputs;
pipeline.executeBatch(frames, outputs, width, height, channels);
std::vector<BatchOutput> outputs;
pipeline.executeBatch(frames, outputs);
```

### Zero-Copy Output
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,24 @@ int main() {
For processing multiple frames efficiently:

```cpp
std::vector<void*> inputs = {...}; // Array of device pointers
std::vector<void*> outputs;
std::vector<ImageBuffer> inputs = {
{frame0, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame1, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
};
std::vector<BatchOutput> outputs;

Pipeline pipeline;
// ... setup pipeline ...

cudaError_t err = pipeline.executeBatch(inputs, outputs, width, height, channels);
cudaError_t err = pipeline.executeBatch(inputs, outputs);
```

The batch executor:
- Processes frames concurrently across multiple streams
- Reuses allocated buffers between frames
- Validates that every frame has identical device-memory shape metadata
- Returns one `BatchOutput` per sink node instead of silently picking one
- Synchronizes only at the end of each batch

## Runtime Parameter Updates
Expand Down
22 changes: 16 additions & 6 deletions docs/zh/api/pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ public:
void* getOutput(int nodeId);

cudaError_t execute();
cudaError_t executeBatch(const std::vector<void*>& inputs,
std::vector<void*>& outputs,
int width, int height, int channels);
cudaError_t executeBatch(const std::vector<ImageBuffer>& inputs,
std::vector<BatchOutput>& outputs);

TaskGraph& getTaskGraph();
const TaskGraph& getTaskGraph() const;
Expand Down Expand Up @@ -91,10 +90,21 @@ void* out = pipeline.getOutput(n3);
### 批量执行

```cpp
std::vector<void*> inputs = {frame0, frame1, frame2};
std::vector<void*> outputs;
std::vector<ImageBuffer> inputs = {
{frame0, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame1, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame2, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
};
std::vector<BatchOutput> outputs;

cudaError_t err = pipeline.executeBatch(inputs, outputs, width, height, channels);
cudaError_t err = pipeline.executeBatch(inputs, outputs);
for (const auto& output : outputs) {
std::cout << "Sink node " << output.nodeId << " produced "
<< output.frames.size() << " frames" << std::endl;
}
```

## 错误处理
Expand Down
10 changes: 6 additions & 4 deletions docs/zh/blog/tutorials/video-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ cudaStreamDestroy(stream);
### 批处理

```cpp
std::vector<void*> frames;
std::vector<ImageBuffer> frames;
for (int i = 0; i < batchSize; i++) {
frames.push_back(getNextFrame());
frames.push_back({getNextFrame(), width, height, channels, width * channels,
sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false});
}

std::vector<void*> outputs;
pipeline.executeBatch(frames, outputs, width, height, channels);
std::vector<BatchOutput> outputs;
pipeline.executeBatch(frames, outputs);
```

### 零拷贝输出
Expand Down
14 changes: 10 additions & 4 deletions docs/zh/guide/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,24 @@ int main() {
高效处理多帧图像:

```cpp
std::vector<void*> inputs = {...}; // 设备指针数组
std::vector<void*> outputs;
std::vector<ImageBuffer> inputs = {
{frame0, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
{frame1, width, height, channels, width * channels, sizeof(uint8_t), 1,
static_cast<size_t>(width) * height * channels, true, false},
};
std::vector<BatchOutput> outputs;

Pipeline pipeline;
// ... 配置流水线 ...

cudaError_t err = pipeline.executeBatch(inputs, outputs, width, height, channels);
cudaError_t err = pipeline.executeBatch(inputs, outputs);
```

批量执行器特性:
- 跨多个流并发处理帧
- 帧间复用已分配缓冲区
- 校验每帧的设备内存形状元数据必须一致
- 每个 sink 节点返回一个 `BatchOutput`,不再静默丢弃额外输出
- 仅在每个批次结束时同步

## 运行时参数更新
Expand Down
35 changes: 26 additions & 9 deletions examples/benchmark_pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ int parseIntArg(char** begin, char** end, const std::string& flag, int defaultVa
return defaultValue;
}

mini_image_pipe::ImageBuffer makeFrameBuffer(void* data, int width, int height, int channels) {
mini_image_pipe::ImageBuffer buffer;
buffer.data = data;
buffer.width = width;
buffer.height = height;
buffer.channels = channels;
buffer.stride = width * channels;
buffer.elementSize = sizeof(uint8_t);
buffer.batchSize = 1;
buffer.batchStride = static_cast<size_t>(buffer.stride) * height;
buffer.isDeviceMemory = true;
return buffer;
}

} // namespace

int main(int argc, char** argv) {
Expand Down Expand Up @@ -64,9 +78,11 @@ int main(int argc, char** argv) {
pipeline.connect(n2, n3);
pipeline.connect(n3, n4);

std::vector<void*> inputs(batchSize, nullptr);
std::vector<void*> outputs;
for (void*& input : inputs) {
std::vector<ImageBuffer> inputs;
inputs.reserve(batchSize);
std::vector<BatchOutput> outputs;
for (int i = 0; i < batchSize; ++i) {
void* input = nullptr;
cudaError_t allocErr = cudaMalloc(&input, imageBytes);
if (allocErr != cudaSuccess) {
std::cerr << "cudaMalloc failed: " << cudaGetErrorString(allocErr) << std::endl;
Expand All @@ -77,18 +93,19 @@ int main(int argc, char** argv) {
std::cerr << "cudaMemset failed: " << cudaGetErrorString(memsetErr) << std::endl;
return 1;
}
inputs.push_back(makeFrameBuffer(input, width, height, channels));
}

const int warmupIterations = 3;
for (int i = 0; i < warmupIterations; ++i) {
if (batchSize == 1) {
pipeline.setInput(n1, inputs.front(), width, height, channels);
pipeline.setInput(n1, inputs.front().data, width, height, channels);
if (pipeline.execute() != cudaSuccess) {
std::cerr << "Warmup execute failed" << std::endl;
return 1;
}
} else {
if (pipeline.executeBatch(inputs, outputs, width, height, channels) != cudaSuccess) {
if (pipeline.executeBatch(inputs, outputs) != cudaSuccess) {
std::cerr << "Warmup executeBatch failed" << std::endl;
return 1;
}
Expand All @@ -98,13 +115,13 @@ int main(int argc, char** argv) {
auto start = std::chrono::steady_clock::now();
for (int i = 0; i < iterations; ++i) {
if (batchSize == 1) {
pipeline.setInput(n1, inputs.front(), width, height, channels);
pipeline.setInput(n1, inputs.front().data, width, height, channels);
if (pipeline.execute() != cudaSuccess) {
std::cerr << "Execute failed" << std::endl;
return 1;
}
} else {
if (pipeline.executeBatch(inputs, outputs, width, height, channels) != cudaSuccess) {
if (pipeline.executeBatch(inputs, outputs) != cudaSuccess) {
std::cerr << "ExecuteBatch failed" << std::endl;
return 1;
}
Expand All @@ -125,8 +142,8 @@ int main(int argc, char** argv) {
std::cout << " total_ms: " << totalMs << std::endl;
std::cout << " fps: " << fps << std::endl;

for (void* input : inputs) {
cudaFree(input);
for (const auto& input : inputs) {
cudaFree(input.data);
}

return 0;
Expand Down
Loading
Loading