Skip to content

Commit

Permalink
feat: init add capi yolov8 ensemble profile implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Jim Wang <[email protected]>
  • Loading branch information
jim-wang-intel committed Feb 13, 2024
1 parent 056f2e6 commit 9165a4b
Show file tree
Hide file tree
Showing 31 changed files with 5,635 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ configs/opencv-ovms/models/2022/person-detection-retail-0013/
configs/opencv-ovms/models/2022/person_vehicle_bike_detection_2000/
configs/opencv-ovms/models/2022/person-vehicle-bike-detection-2000/
configs/opencv-ovms/models/2022/text-detect-0002/
configs/opencv-ovms/models/2022/yolov8/
configs/opencv-ovms/grpc_go/results/
configs/opencv-ovms/cmd_client/ovms-client
configs/opencv-ovms/cmd_client/profile-launcher
Expand Down
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
# SPDX-License-Identifier: Apache-2.0

.PHONY: build-dlstreamer build-dlstreamer-realsense build-grpc-python build-grpc-go build-python-apps build-telegraf
.PHONY: build-capi_face_detection build-capi_yolov5 build-capi_yolov5_ensemble
.PHONY: build-capi_face_detection build-capi_yolov5 build-capi_yolov5_ensemble build-capi_yolov8_ensemble
.PHONY: run-camera-simulator run-telegraf run-portainer run-pipelines
.PHONY: clean-grpc-go clean-segmentation clean-ovms clean-all clean-results clean-telegraf clean-models clean-webcam
.PHONY: clean-ovms-server-configs clean-ovms-server
.PHONY: down-portainer down-pipelines
.PHONY: clean clean-simulator clean-object-detection clean-classification clean-gst clean-capi_face_detection clean-capi_yolov5 clean-capi_yolov5_ensemble
.PHONY: clean clean-simulator clean-object-detection clean-classification clean-gst clean-capi_face_detection clean-capi_yolov5 clean-capi_yolov5_ensemble clean-capi_yolov8_ensemble
.PHONY: list-profiles
.PHONY: unit-test-profile-launcher build-profile-launcher profile-launcher-status clean-profile-launcher webcam-rtsp
.PHONY: clean-test
Expand Down Expand Up @@ -60,7 +60,7 @@ build-profile-launcher:
build-ovms-server:
HTTPS_PROXY=${HTTPS_PROXY} HTTP_PROXY=${HTTP_PROXY} docker pull openvino/model_server:2023.1-gpu

clean-profile-launcher: clean-grpc-python clean-grpc-go clean-segmentation clean-object-detection clean-classification clean-gst clean-capi_face_detection clean-test clean-capi_yolov5 clean-capi_yolov5_ensemble
clean-profile-launcher: clean-grpc-python clean-grpc-go clean-segmentation clean-object-detection clean-classification clean-gst clean-capi_face_detection clean-test clean-capi_yolov5 clean-capi_yolov5_ensemble clean-capi_yolov8_ensemble
@echo "containers launched by profile-launcher are cleaned up."
@pkill -9 profile-launcher || true

Expand Down Expand Up @@ -103,6 +103,9 @@ clean-capi_yolov5:
clean-capi_yolov5_ensemble:
./clean-containers.sh capi_yolov5_ensemble

clean-capi_yolov8_ensemble:
./clean-containers.sh capi_yolov8_ensemble

clean-telegraf:
./clean-containers.sh influxdb2
./clean-containers.sh telegraf
Expand Down Expand Up @@ -164,6 +167,9 @@ build-capi_yolov5: build-profile-launcher
build-capi_yolov5_ensemble: build-profile-launcher
cd configs/opencv-ovms/gst_capi && DGPU_TYPE=$(DGPU_TYPE) $(MAKE) build_capi_yolov5_ensemble

build-capi_yolov8_ensemble: build-profile-launcher
cd configs/opencv-ovms/gst_capi && DGPU_TYPE=$(DGPU_TYPE) $(MAKE) build_capi_yolov8_ensemble

clean-docs:
rm -rf docs/

Expand Down
26 changes: 21 additions & 5 deletions configs/opencv-ovms/cmd_client/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ----------------------------------------------------------------------------------
// Copyright 2023 Intel Corp.
// Copyright 2024 Intel Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -124,6 +124,7 @@ type DockerLauncherInfo struct {

type OvmsClientInfo struct {
DockerLauncher DockerLauncherInfo
OVMSCustomNodeJson string
PipelineScript string
PipelineInputArgs string
PipelineStreamDensityRun string
Expand Down Expand Up @@ -192,7 +193,7 @@ func main() {
// as the client itself has it like C-Api case
if ovmsClientConf.OvmsSingleContainer {
log.Println("running in single container mode, no distributed client-server")
ovmsClientConf.generateConfigJsonForCApi()
ovmsClientConf.generateConfigJsonForCApi(flags.configDir)
} else {
// launcher ovms server
ovmsClientConf.startOvmsServer()
Expand Down Expand Up @@ -273,10 +274,26 @@ func (ovmsClientConf *OvmsClientConfig) setEnvContainerCountAndGrpcPort() {
log.Println("GRPC_PORT=", os.Getenv(GRPC_PORT_ENV))
}

func (ovmsClientConf *OvmsClientConfig) generateConfigJsonForCApi() {
func (ovmsClientConf *OvmsClientConfig) generateConfigJsonForCApi(configDir string) {
log.Println("generate and update config json file for C-API case...")

deviceUpdater := server.NewDeviceUpdater(ovmsConfigJsonDir, ovmsTemplateConfigJson)
customNodeJsonConfigFile := strings.TrimSpace(ovmsClientConf.OvmsClient.OVMSCustomNodeJson)

deviceConfigJsonFileInput := ovmsTemplateConfigJson
if len(customNodeJsonConfigFile) > 0 {
log.Printf("use custom node json from %s", customNodeJsonConfigFile)
pipelineProfile := strings.TrimSpace(os.Getenv(pipelineProfileEnv))
customNodeJsonConfigFilePath := filepath.Join(configDir, resourceDir, pipelineProfile, customNodeJsonConfigFile)
customNodeUpdater := server.NewCustomNodeUpdater(customNodeJsonConfigFilePath, ovmsConfigJsonDir, ovmsTemplateConfigJson)
newUpdateConfigJson := "config_ovms-server_" + ovmsClientConf.OvmsClient.DockerLauncher.ContainerName + os.Getenv(CID_COUNT_ENV) + ".json"
if err := customNodeUpdater.UpdateCustomNode(filepath.Join(ovmsConfigJsonDir, newUpdateConfigJson)); err != nil {
log.Printf("Error: failed to update custom node information and produce a new ovms server config json: %v", err)
os.Exit(1)
}
deviceConfigJsonFileInput = newUpdateConfigJson
}

deviceUpdater := server.NewDeviceUpdater(ovmsConfigJsonDir, deviceConfigJsonFileInput)
targetDevice := defaultTargetDevice
if len(os.Getenv(TARGET_DEVICE_ENV)) > 0 {
// only set the value from env if env is not empty; otherwise defaults to the default value in defaultTargetDevice
Expand All @@ -287,7 +304,6 @@ func (ovmsClientConf *OvmsClientConfig) generateConfigJsonForCApi() {
log.Println("Updating config with DEVICE environment variable:", targetDevice)

newUpdateConfigJson := "config_ovms-server_" + ovmsClientConf.OvmsClient.DockerLauncher.ContainerName + os.Getenv(CID_COUNT_ENV) + ".json"

if err := deviceUpdater.UpdateDeviceAndCreateJson(targetDevice, filepath.Join(ovmsConfigJsonDir, newUpdateConfigJson)); err != nil {
log.Printf("Error: failed to update device and produce a new ovms server config json: %v", err)
os.Exit(1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
OvmsSingleContainer: true
OvmsClient:
DockerLauncher:
Script: docker-launcher.sh
DockerImage: openvino/model_server-capi-gst-ovms-capi_yolov8_ensemble:latest
ContainerName: capi_yolov8_ensemble
Volumes:
- "$cl_cache_dir:/home/intel/gst-ovms/.cl-cache"
- /tmp/.X11-unix:/tmp/.X11-unix
- "$RUN_PATH/sample-media/:/home/intel/gst-ovms/vids"
- "$RUN_PATH/configs/opencv-ovms/gst_capi/extensions:/home/intel/gst-ovms/extensions"
- "$RUN_PATH/results:/tmp/results"
- "$RUN_PATH/configs/opencv-ovms/models/2022/:/models"
# when the OVMSCustomNodeJson is not empty, then it will add or replace the existing customNode info for CAPI-OVMS server config
OVMSCustomNodeJson: yolov8_custom_node.json
PipelineScript: ./run_gst_capi.sh
PipelineInputArgs: "" # space delimited like we run the script in command and take those input arguments
EnvironmentVariableFiles:
- capi_yolov8_ensemble.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"custom_node_library_config_list": [
{
"name": "efficientnetb0_extractor",
"base_path": "/ovms/lib/libcustom_node_efficientnetb0-yolov8.so"
}
],
"pipeline_config_list": [
{
"name": "detect_classify",
"inputs": ["images"],
"nodes": [
{
"name": "detection_node",
"model_name": "yolov8",
"type": "DL model",
"inputs": [
{"images": {"node_name": "request",
"data_item": "images"}
}
],
"outputs": [
{"data_item": "output0",
"alias": "boxes"}
]
},
{
"name": "extract_node",
"library_name": "efficientnetb0_extractor",
"type": "custom",
"demultiply_count": 0,
"params": {
"original_image_width": "416",
"original_image_height": "416",
"original_image_layout": "NCHW",
"target_image_width": "224",
"target_image_height": "224",
"target_image_layout": "NCHW",
"convert_to_gray_scale": "false",
"confidence_threshold": "0.5",
"max_output_batch": "100",
"debug": "false"
},
"inputs": [
{"images": {"node_name": "request",
"data_item": "images"}},
{"boxes": {"node_name": "detection_node",
"data_item": "boxes"}}
],
"outputs": [
{"data_item": "roi_images",
"alias": "roi_images"},
{"data_item": "roi_coordinates",
"alias": "roi_coordinates"},
{"data_item": "confidence_levels",
"alias": "confidence_levels"}
]
},
{
"name": "classification_node",
"model_name": "efficientnetb0_FP32INT8",
"type": "DL model",
"inputs": [
{"sub": {"node_name": "extract_node",
"data_item": "roi_images"}}
],
"outputs": [
{"data_item": "efficientnet-b0/model/head/dense/BiasAdd/Add",
"alias": "classify_output"}
]
}
],
"outputs": [
{"roi_images": {"node_name": "extract_node",
"data_item": "roi_images"}},
{"roi_coordinates": {"node_name": "extract_node",
"data_item": "roi_coordinates"}},
{"confidence_levels": {"node_name": "extract_node",
"data_item": "confidence_levels"}},
{"classify_output": {"node_name": "classification_node",
"data_item": "classify_output"}}
]
}
]
}
90 changes: 90 additions & 0 deletions configs/opencv-ovms/cmd_client/server/customNode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// ----------------------------------------------------------------------------------
// Copyright 2024 Intel Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------------------------------------------------------------

package server

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)

type CustomNodeUpdater struct {
customNodeConfigJsonFile string
templateConfigJsonDir string
templateConfigJsonFileName string
}

type OvmsCustomNodeConfig struct {
CustomNode []map[string]interface{} `json:"custom_node_library_config_list"`
PipelineConfig []map[string]interface{} `json:"pipeline_config_list"`
}

func NewCustomNodeUpdater(customNodeConfigJsonFile string, templateConfigJsonDir, templateConfigJsonFileName string) *CustomNodeUpdater {
return &CustomNodeUpdater{
customNodeConfigJsonFile: customNodeConfigJsonFile,
templateConfigJsonDir: templateConfigJsonDir,
templateConfigJsonFileName: templateConfigJsonFileName,
}
}

// UpdateCustomNode adds (if not exists) or updates custom_node_library_config_list and related json information from customNodeConfigJsonFile into
// the template config json file and then produces a new config json file based on the input newConfigJsonFile;
// it returns error if failed to parse json or failed to produce a new config json file
func (cu *CustomNodeUpdater) UpdateCustomNode(newConfigJsonFile string) error {
templateConfigJsonFile := filepath.Join(cu.templateConfigJsonDir, cu.templateConfigJsonFileName)
contents, err := os.ReadFile(templateConfigJsonFile)
if err != nil {
return fmt.Errorf("CustomNodeUpdater failed to read OVMS template config json file %s: %v", templateConfigJsonFile, err)
}

var ovmsConfigData OvmsConfig
err = json.Unmarshal(contents, &ovmsConfigData)
if err != nil {
return fmt.Errorf("CustomNodeUpdater parsing OVMS template config json error: %v", err)
}

// read also the customNodeConfigJsonFile:
customNodeConfig, err := os.ReadFile(cu.customNodeConfigJsonFile)
if err != nil {
return fmt.Errorf("CustomNodeUpdater failed to read OVMS custom node json file %s: %v", cu.customNodeConfigJsonFile, err)
}

var customNodeData OvmsCustomNodeConfig
err = json.Unmarshal(customNodeConfig, &customNodeData)
if err != nil {
return fmt.Errorf("CustomNodeUpdater parsing OVMS custom node json error: %v", err)
}

if len(customNodeData.CustomNode) > 0 {
// replacing:
ovmsConfigData.CustomNode = customNodeData.CustomNode
ovmsConfigData.PipelineConfig = customNodeData.PipelineConfig
}

updateConfig, err := json.MarshalIndent(ovmsConfigData, "", " ")
if err != nil {
return fmt.Errorf("CustomNodeUpdater could not marshal config to JSON: %v", err)
}

if err := os.WriteFile(newConfigJsonFile, updateConfig, 0644); err != nil {
return fmt.Errorf("CustomNodeUpdater could not write a updated config to a new JSON: %v", err)
}

return nil
}
67 changes: 67 additions & 0 deletions configs/opencv-ovms/cmd_client/server/customNode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// ----------------------------------------------------------------------------------
// Copyright 2024 Intel Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------------------------------------------------------------

package server

import (
"os"
"testing"

"github.com/stretchr/testify/require"
)

func TestCustomNodes(t *testing.T) {
tests := []struct {
name string
customNodeJsonFile string
templateJsonFileDir string
templateJsonFileName string
newJsonFileName string
expectError bool
expectedCustomNodeLib string
}{
{"add a new test custom node", "./test-yolov8_custom_node.json", ".", "test-update-device.json", "./add-test-yolov8.json", false, "/ovms/lib/libcustom_node_efficientnetb0-yolov8.so"},
{"replacing an existing custom node", "./test-yolov8_custom_node.json", ".", "test-customnode.json", "./a-replacing-test-yolov8.json", false, "/ovms/lib/libcustom_node_efficientnetb0-yolov8.so"},
{"non-existing custom node file", "./non-existing", ".", "test-update-device.json", "./failed-non-existing.json", true, ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
customNodeUpdater := NewCustomNodeUpdater(tt.customNodeJsonFile, tt.templateJsonFileDir, tt.templateJsonFileName)

err := customNodeUpdater.UpdateCustomNode(tt.newJsonFileName)
defer func() {
_ = os.Remove(tt.newJsonFileName)
}()

if !tt.expectError {
require.NoError(t, err)
require.NotEmpty(t, customNodeUpdater)
require.FileExists(t, tt.newJsonFileName)
newData, readErr := os.ReadFile(tt.newJsonFileName)
require.NoError(t, readErr)
require.Contains(t, string(newData), "\"base_path\": \""+tt.expectedCustomNodeLib+"\"")
if tt.templateJsonFileName == "test-customnode.json" {
require.Contains(t, string(newData), "\"custom_node_library_config_list\":")
require.Contains(t, string(newData), "\"pipeline_config_list\":")
}
} else {
require.Error(t, err)
}
})
}
}
Loading

0 comments on commit 9165a4b

Please sign in to comment.