From 17b442e929cb1d87ccf6072ea71706a53057781f Mon Sep 17 00:00:00 2001 From: Daniel Brondani Date: Wed, 6 May 2026 18:19:04 +0200 Subject: [PATCH 1/3] [projmgr] Introduce MLOps management --- tools/projmgr/CMakeLists.txt | 3 +- tools/projmgr/include/ProjMgr.h | 2 + tools/projmgr/include/ProjMgrMlops.h | 138 +++++++++ tools/projmgr/include/ProjMgrParser.h | 66 ++++ tools/projmgr/include/ProjMgrWorker.h | 41 ++- tools/projmgr/include/ProjMgrYamlEmitter.h | 10 +- tools/projmgr/include/ProjMgrYamlParser.h | 15 +- .../projmgr/schemas/cbuild-mlops.schema.json | 15 + tools/projmgr/schemas/common.schema.json | 116 +++++++ tools/projmgr/src/ProjMgr.cpp | 12 + tools/projmgr/src/ProjMgrCbuildMlops.cpp | 102 +++++++ tools/projmgr/src/ProjMgrMlops.cpp | 285 ++++++++++++++++++ tools/projmgr/src/ProjMgrYamlParser.cpp | 41 +++ .../projmgr/src/ProjMgrYamlSchemaChecker.cpp | 2 +- .../data/MLOps/ai_layer/ai_layer.clayer.yml | 3 + .../test/data/MLOps/core0/core0.cproject.yml | 5 + .../test/data/MLOps/core1/core1.cproject.yml | 5 + .../test/data/MLOps/extended.csolution.yml | 53 ++++ .../test/data/MLOps/failure1.csolution.yml | 18 ++ .../test/data/MLOps/failure2.csolution.yml | 18 ++ .../test/data/MLOps/failure3.csolution.yml | 20 ++ .../test/data/MLOps/failure4.csolution.yml | 28 ++ .../test/data/MLOps/fvp/fvp_config.txt | 1 + .../test/data/MLOps/minimal.csolution.yml | 40 +++ .../data/MLOps/npu_macs_only.csolution.yml | 32 ++ .../data/MLOps/npu_type_only.csolution.yml | 32 ++ .../data/MLOps/ref/extended.cbuild-mlops.yml | 31 ++ .../data/MLOps/ref/minimal.cbuild-mlops.yml | 31 ++ tools/projmgr/test/data/MLOps/vela/custom.ini | 1 + tools/projmgr/test/src/ProjMgrUnitTests.cpp | 66 ++++ 30 files changed, 1224 insertions(+), 8 deletions(-) create mode 100644 tools/projmgr/include/ProjMgrMlops.h create mode 100644 tools/projmgr/schemas/cbuild-mlops.schema.json create mode 100644 tools/projmgr/src/ProjMgrCbuildMlops.cpp create mode 100644 tools/projmgr/src/ProjMgrMlops.cpp create mode 100644 tools/projmgr/test/data/MLOps/ai_layer/ai_layer.clayer.yml create mode 100644 tools/projmgr/test/data/MLOps/core0/core0.cproject.yml create mode 100644 tools/projmgr/test/data/MLOps/core1/core1.cproject.yml create mode 100644 tools/projmgr/test/data/MLOps/extended.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/failure1.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/failure2.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/failure3.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/failure4.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/fvp/fvp_config.txt create mode 100644 tools/projmgr/test/data/MLOps/minimal.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/npu_macs_only.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/npu_type_only.csolution.yml create mode 100644 tools/projmgr/test/data/MLOps/ref/extended.cbuild-mlops.yml create mode 100644 tools/projmgr/test/data/MLOps/ref/minimal.cbuild-mlops.yml create mode 100644 tools/projmgr/test/data/MLOps/vela/custom.ini diff --git a/tools/projmgr/CMakeLists.txt b/tools/projmgr/CMakeLists.txt index 90eaa5853..020f1d682 100644 --- a/tools/projmgr/CMakeLists.txt +++ b/tools/projmgr/CMakeLists.txt @@ -39,13 +39,14 @@ SET(PROJMGR_SOURCE_FILES ProjMgr.cpp ProjMgrKernel.cpp ProjMgrCallback.cpp ProjMgrCbuildBase.cpp ProjMgrCbuild.cpp ProjMgrCbuildIdx.cpp ProjMgrCbuildGenIdx.cpp ProjMgrCbuildPack.cpp ProjMgrCbuildSet.cpp ProjMgrCbuildRun.cpp ProjMgrRunDebug.cpp + ProjMgrCbuildMlops.cpp ProjMgrMlops.cpp ProjMgrRpcServer.cpp ProjMgrRpcServerData.cpp ) SET(PROJMGR_HEADER_FILES ProjMgr.h ProjMgrKernel.h ProjMgrCallback.h ProjMgrParser.h ProjMgrWorker.h ProjMgrGenerator.h ProjMgrXmlParser.h ProjMgrYamlParser.h ProjMgrLogger.h ProjMgrYamlSchemaChecker.h ProjMgrYamlEmitter.h ProjMgrUtils.h ProjMgrExtGenerator.h - ProjMgrCbuildBase.h ProjMgrRunDebug.h + ProjMgrCbuildBase.h ProjMgrRunDebug.h ProjMgrMlops.h ProjMgrRpcServer.h ProjMgrRpcServerData.h ) diff --git a/tools/projmgr/include/ProjMgr.h b/tools/projmgr/include/ProjMgr.h index 240aac121..29bf0615a 100644 --- a/tools/projmgr/include/ProjMgr.h +++ b/tools/projmgr/include/ProjMgr.h @@ -12,6 +12,7 @@ #include "ProjMgrGenerator.h" #include "ProjMgrYamlEmitter.h" #include "ProjMgrRunDebug.h" +#include "ProjMgrMlops.h" #include "ProjMgrRpcServer.h" #include @@ -192,6 +193,7 @@ class ProjMgr { ProjMgrGenerator m_generator; ProjMgrYamlEmitter m_emitter; ProjMgrRunDebug m_runDebug; + ProjMgrMlops m_mlops; ProjMgrRpcServer m_rpcServer; std::string m_csolutionFile; diff --git a/tools/projmgr/include/ProjMgrMlops.h b/tools/projmgr/include/ProjMgrMlops.h new file mode 100644 index 000000000..bf7193150 --- /dev/null +++ b/tools/projmgr/include/ProjMgrMlops.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020-2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef PROJMGRMLOPS_H +#define PROJMGRMLOPS_H + +#include "ProjMgrWorker.h" + +struct CsolutionItem; +struct ContextItem; +struct CustomItem; +struct MlopsItem; +struct TargetSetItem; +struct TargetType; + +/** + * @brief mlops processor type containing + * processor type +*/ +struct MlopsProcessorType { + std::string type; +}; + +/** + * @brief mlops NPU type containing + * NPU type, + * number of MACs +*/ +struct MlopsNpuType { + std::string type; + std::string macs; +}; + +/** + * @brief mlops Vela type containing + * path to INI file, + * option string +*/ +struct MlopsVelaType { + std::string ini; + std::string options; +}; + +/** + * @brief mlops model type containing + * path to AI clayer, + * model name +*/ +struct MlopsModelType { + std::string clayer; + std::string name; +}; + +/** + * @brief Mlops output type +*/ +struct MlopsOutputType { + std::string file; + std::string type; +}; + +/** + * @brief mlops run type containing + * active target-set, + * cbuild-run file + * output artifacts +*/ +struct MlopsRunType { + std::string active; + std::string cbuildRun; + std::vector output; +}; + +/** + * @brief mlops simulator type containing + * active target-set, + * cbuild-run file + * output artifacts + * simulator model, + * simulator configuration file +*/ +struct MlopsSimulatorType : MlopsRunType { + std::string model; + std::string configFile; +}; + +/** + * @brief mlops type +*/ +struct MlopsType { + std::string description; + MlopsProcessorType processor; + MlopsNpuType npu; + MlopsVelaType vela; + MlopsModelType model; + MlopsRunType hardware; + MlopsSimulatorType simulator; +}; + +/** + * @brief projmgr mlops management class +*/ +class ProjMgrMlops { +public: + /** + * @brief class constructor + */ + ProjMgrMlops(ProjMgrWorker* worker); + + /** + * @brief class destructor + */ + ~ProjMgrMlops(void); + + /** + * @brief collect mlops information + * @param csolution csolution settings + * @param mlops output settings + * @return true if executed successfully + */ + bool CollectSettings(const CsolutionItem& csolution, MlopsType& mlops); + +private: + ProjMgrWorker* m_worker = nullptr; + bool FindTargetType(const CsolutionItem& csolution, const std::string& typeName, TargetType& targetType) const; + bool GetTargetSetItemRef(const TargetType& targetType, const std::string& targetTypeName, + const std::string& targetSetName, bool simulatorDefault, TargetSetItem& targetSet) const; + std::string BuildActive(const std::string& targetType, const std::string& targetSet) const; + std::string GetCustomScalar(const CustomItem& custom, const std::string& key) const; + std::string BuildVelaOptions(const MlopsNpuType& npu, const MlopsVelaItem& vela) const; + void SetMlopsRunType(MlopsRunType& run, const std::string& targetType, const std::string& targetSet, + const std::vector& contexts, const std::string& outBaseDir, const std::string& solutionName) const; +}; + +#endif // PROJMGRMLOPS_H diff --git a/tools/projmgr/include/ProjMgrParser.h b/tools/projmgr/include/ProjMgrParser.h index 64870dd30..9cf78a769 100644 --- a/tools/projmgr/include/ProjMgrParser.h +++ b/tools/projmgr/include/ProjMgrParser.h @@ -542,6 +542,71 @@ struct CdefaultItem { typedef std::vector> BuildTypes; typedef std::vector> TargetTypes; + +/** + * @brief mlops NPU item containing + * NPU type, + * number of MACs +*/ +struct MlopsNpuItem { + std::string type; + std::string macs; +}; + +/** + * @brief mlops Vela item containing + * path to INI file, + * system configuration selector, + * memory configuration selector, + * additional Vela options string +*/ +struct MlopsVelaItem { + std::string ini; + std::string system; + std::string memory; + std::string misc; +}; + +/** + * @brief mlops model item containing + * path to AI clayer, + * model name +*/ +struct MlopsModelItem { + std::string clayer; + std::string name; +}; + +/** + * @brief mlops hardware/simulator target item containing + * explicit target-type name, + * explicit target-set name +*/ +struct MlopsTargetItem { + std::string targetType; + std::string targetSet; +}; + +/** + * @brief mlops item containing + * enable flag + * description, + * NPU configuration, + * Vela compiler configuration, + * ML model configuration, + * hardware target for testing, + * simulator target for testing +*/ +struct MlopsItem { + bool enabled = false; + std::string description; + MlopsNpuItem npu; + MlopsVelaItem vela; + MlopsModelItem model; + MlopsTargetItem hardware; + MlopsTargetItem simulator; +}; + /** * @brief solution item containing * csolution name, @@ -583,6 +648,7 @@ struct CsolutionItem { std::vector executes; std::vector ymlOrderedBuildTypes; std::vector ymlOrderedTargetTypes; + MlopsItem mlops; }; /** diff --git a/tools/projmgr/include/ProjMgrWorker.h b/tools/projmgr/include/ProjMgrWorker.h index 80771b888..96db82aaf 100644 --- a/tools/projmgr/include/ProjMgrWorker.h +++ b/tools/projmgr/include/ProjMgrWorker.h @@ -1175,6 +1175,42 @@ class ProjMgrWorker { */ bool ElaborateVariablesConfigurations(); + /** + * @brief Resolve access sequences and normalize a path relative to a reference directory. + * + * This expands static and dynamic access sequences in `item`, optionally records + * context dependencies for cross-context references, and converts the final path + * to be relative to `outDir` using `ref` as the input base directory when no + * context-based replacement is performed. + * + * @param context current context used for variable expansion and dependency tracking + * @param item input string to process; replaced in-place with the resolved value + * @param ref base directory used to resolve relative paths + * @param genDep add referenced contexts to `context.dependsOn` when true + * @param outDir output directory used when rebasing the resulting path; defaults to the context cprj directory when empty + * @param withHeadingDot preserve a leading `./` when generating relative paths + * @param solutionLevel allow solution-level context matching for access sequences + * @return true if the sequence was resolved successfully, otherwise false + */ + bool ProcessSequenceRelative(ContextItem& context, std::string& item, const std::string& ref = std::string(), + bool genDep = true, std::string outDir = std::string(), bool withHeadingDot = false, bool solutionLevel = false); + + /** + * @brief parse and load context layers + * @param context item + * @return true if there is no error + */ + bool ParseContextLayers(ContextItem& context); + + /** + * @brief process context precedences + * @param context item + * @param scope: process board, device or both + * @param rerun flag to reprocess + * @return true if there is no error + */ + bool ProcessPrecedences(ContextItem& context, BoardOrDevice process = BoardOrDevice::None, bool rerun = false); + /** * @brief clear worker members for reloading a solution * @return true if there is no error @@ -1259,7 +1295,6 @@ class ProjMgrWorker { bool CheckContextFilters(const TypeFilter& typeFilter, const ContextItem& context); bool GetTypeContent(ContextItem& context); bool GetProjectSetup(ContextItem& context); - bool ProcessPrecedences(ContextItem& context, BoardOrDevice process = BoardOrDevice::None, bool rerun = false); bool ProcessPrecedence(StringCollection& item); bool ProcessCompilerPrecedence(StringCollection& item, bool acceptRedefinition = false); bool ProcessDevicePrecedence(StringCollection& item); @@ -1279,8 +1314,7 @@ class ProjMgrWorker { bool ProcessSequencesRelatives(ContextItem& context, bool rerun); bool ProcessSequencesRelatives(ContextItem& context, std::vector& src, const std::string& ref = std::string(), std::string outDir = std::string(), bool withHeadingDot = false, bool solutionLevel = false); bool ProcessSequencesRelatives(ContextItem& context, BuildType& build, const std::string& ref = std::string()); - bool ProcessSequenceRelative(ContextItem& context, std::string& item, const std::string& ref = std::string(), bool genDep = true, std::string outDir = std::string(), bool withHeadingDot = false, bool solutionLevel = false); - bool ProcessOutputFilenames(ContextItem& context); + bool ProcessOutputFilenames(ContextItem& context); bool ProcessLinkerOptions(ContextItem& context); bool ProcessLinkerOptions(ContextItem& context, const LinkerItem& linker, const std::string& ref); bool ProcessProcessorOptions(ContextItem& context); @@ -1336,7 +1370,6 @@ class ProjMgrWorker { bool GetGeneratorDir(const RteGenerator* generator, ContextItem& context, const std::string& layer, std::string& genDir); bool GetGeneratorOptions(ContextItem& context, const std::string& layer, GeneratorOptionsItem& options); bool GetExtGeneratorOptions(ContextItem& context, const std::string& layer, GeneratorOptionsItem& options); - bool ParseContextLayers(ContextItem& context); bool AddPackRequirements(ContextItem& context, const std::vector& packRequirements); void InsertPackRequirements(const std::vector& src, std::vector& dst, std::string base); void CheckTypeFilterSpelling(const TypeFilter& typeFilter); diff --git a/tools/projmgr/include/ProjMgrYamlEmitter.h b/tools/projmgr/include/ProjMgrYamlEmitter.h index f8d99f406..70c10d538 100644 --- a/tools/projmgr/include/ProjMgrYamlEmitter.h +++ b/tools/projmgr/include/ProjMgrYamlEmitter.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2025 Arm Limited. All rights reserved. + * Copyright (c) 2020-2026 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 */ @@ -7,6 +7,7 @@ #ifndef PROJMGRYAMLEMITTER_H #define PROJMGRYAMLEMITTER_H +#include "ProjMgrMlops.h" #include "ProjMgrRunDebug.h" #include "ProjMgrWorker.h" @@ -103,6 +104,13 @@ class ProjMgrYamlEmitter { */ bool GenerateCbuildRun(const RunDebugType& debugRun); + /** + * @brief generate cbuild mlops file + * @param reference to struct with mlops info + * @return true if executed successfully + */ + bool GenerateMlops(const MlopsType& mlops); + protected: ProjMgrParser* m_parser = nullptr; ProjMgrWorker* m_worker = nullptr; diff --git a/tools/projmgr/include/ProjMgrYamlParser.h b/tools/projmgr/include/ProjMgrYamlParser.h index 505a8dc8d..9e6c4d69f 100644 --- a/tools/projmgr/include/ProjMgrYamlParser.h +++ b/tools/projmgr/include/ProjMgrYamlParser.h @@ -56,6 +56,7 @@ static constexpr const char* YAML_CBUILDS = "cbuilds"; static constexpr const char* YAML_CBUILD = "cbuild"; static constexpr const char* YAML_CBUILD_GENS = "cbuild-gens"; static constexpr const char* YAML_CBUILD_GEN = "cbuild-gen"; +static constexpr const char* YAML_CBUILD_MLOPS = "cbuild-mlops"; static constexpr const char* YAML_CBUILD_PACK = "cbuild-pack"; static constexpr const char* YAML_CBUILD_RUN = "cbuild-run"; static constexpr const char* YAML_CBUILD_SET = "cbuild-set"; @@ -74,6 +75,7 @@ static constexpr const char* YAML_COMPILER = "compiler"; static constexpr const char* YAML_COMPONENT = "component"; static constexpr const char* YAML_COMPONENTS = "components"; static constexpr const char* YAML_CONDITION = "condition"; +static constexpr const char* YAML_CONFIG_FILE = "config-file"; static constexpr const char* YAML_CONFIGURATION = "configuration"; static constexpr const char* YAML_CONFIGURATIONS = "configurations"; static constexpr const char* YAML_CONNECT = "connect"; @@ -130,6 +132,7 @@ static constexpr const char* YAML_FORPROJECTPART = "for-project-part"; static constexpr const char* YAML_FPU = "fpu"; static constexpr const char* YAML_GDBSERVER = "gdbserver"; static constexpr const char* YAML_GENERATED_BY = "generated-by"; +static constexpr const char* YAML_HARDWARE = "hardware"; static constexpr const char* YAML_GENERATOR = "generator"; static constexpr const char* YAML_GENERATORS = "generators"; static constexpr const char* YAML_GENERATOR_IMPORT = "generator-import"; @@ -141,6 +144,7 @@ static constexpr const char* YAML_HPROT = "HPROT"; static constexpr const char* YAML_ID = "id"; static constexpr const char* YAML_IF = "if"; static constexpr const char* YAML_IMAGES = "images"; +static constexpr const char* YAML_INI = "ini"; static constexpr const char* YAML_IMAGE = "image"; static constexpr const char* YAML_IMAGE_ONLY = "image-only"; static constexpr const char* YAML_IMPLEMENTED_BY = "implemented-by"; @@ -165,6 +169,8 @@ static constexpr const char* YAML_LINKER = "linker"; static constexpr const char* YAML_LINK_TIME_OPTIMIZE = "link-time-optimize"; static constexpr const char* YAML_MAP = "map"; static constexpr const char* YAML_MACS = "macs"; +static constexpr const char* YAML_MLOPS = "mlops"; +static constexpr const char* YAML_MODEL = "model"; static constexpr const char* YAML_MASK = "mask"; static constexpr const char* YAML_MAX_CLOCK = "max-clock"; static constexpr const char* YAML_MAX_INSTANCES = "maxInstances"; @@ -185,10 +191,12 @@ static constexpr const char* YAML_MPU = "mpu"; static constexpr const char* YAML_MVE = "mve"; static constexpr const char* YAML_NAME = "name"; static constexpr const char* YAML_NOTFORCONTEXT = "not-for-context"; +static constexpr const char* YAML_NPU = "npu"; static constexpr const char* YAML_NPU_INFO = "npu-info"; static constexpr const char* YAML_OPTIMIZE = "optimize"; static constexpr const char* YAML_OPTIONAL = "optional"; static constexpr const char* YAML_OPTIONS = "options"; +static constexpr const char* YAML_OUT_DIR = "out-dir"; static constexpr const char* YAML_OUTPUT = "output"; static constexpr const char* YAML_OUTPUTDIRS = "output-dirs"; static constexpr const char* YAML_OUTPUT_CPRJDIR = "cprjdir"; @@ -233,6 +241,7 @@ static constexpr const char* YAML_SCRIPT = "script"; static constexpr const char* YAML_SDF = "sdf"; static constexpr const char* YAML_SELECT = "select"; static constexpr const char* YAML_SELECTED_BY = "selected-by"; +static constexpr const char* YAML_SIMULATOR = "simulator"; static constexpr const char* YAML_SELECTED_BY_PACK = "selected-by-pack"; static constexpr const char* YAML_SETUPS = "setups"; static constexpr const char* YAML_SETUP = "setup"; @@ -245,6 +254,7 @@ static constexpr const char* YAML_START_PNAME = "start-pname"; static constexpr const char* YAML_STATUS = "status"; static constexpr const char* YAML_SOLUTION = "solution"; static constexpr const char* YAML_SPROT = "SPROT"; +static constexpr const char* YAML_SYSTEM = "system"; static constexpr const char* YAML_SWD = "swd"; static constexpr const char* YAML_SWJ = "swj"; static constexpr const char* YAML_SWITCH = "switch"; @@ -252,7 +262,8 @@ static constexpr const char* YAML_SYSTEM_DESCRIPTIONS = "system-descriptions"; static constexpr const char* YAML_SYSTEM_RESOURCES = "system-resources"; static constexpr const char* YAML_SYSTEMVIEW = "systemview"; static constexpr const char* YAML_TAPINDEX = "tapindex"; -static constexpr const char* YAML_TARGET_CONFIGURATIONS = "target-configurations"; +static constexpr const char* YAML_TARGET = "target"; + static constexpr const char* YAML_TARGET_CONFIGURATIONS = "target-configurations"; static constexpr const char* YAML_TARGETSEL = "targetsel"; static constexpr const char* YAML_TARGET_SET = "target-set"; static constexpr const char* YAML_TARGETTYPE = "target-type"; @@ -268,6 +279,7 @@ static constexpr const char* YAML_UPDATE = "update"; static constexpr const char* YAML_VARIABLES = "variables"; static constexpr const char* YAML_VARS = "vars"; static constexpr const char* YAML_VALUE = "value"; +static constexpr const char* YAML_VELA = "vela"; static constexpr const char* YAML_VELA_INI = "vela-ini"; static constexpr const char* YAML_VERSION = "version"; static constexpr const char* YAML_WARNINGS = "warnings"; @@ -366,6 +378,7 @@ class ProjMgrYamlParser { void ParseOutputDirs(const YAML::Node& parent, const std::string& file, struct DirectoriesItem& directories); void ParseGenerators(const YAML::Node& parent, const std::string& file, GeneratorsItem& generators); void ParseExecutes(const YAML::Node& parent, const std::string& file, std::vector& executes); + void ParseMlops(const YAML::Node& parent, const std::string& file, MlopsItem& mlops); void ParseDebugger(const YAML::Node& parent, const std::string& file, DebuggerItem& debugger); void ParseTargetSet(const YAML::Node& parent, const std::string& file, std::vector& targetSet); void ParseImages(const YAML::Node& parent, const std::string& file, std::vector& loads); diff --git a/tools/projmgr/schemas/cbuild-mlops.schema.json b/tools/projmgr/schemas/cbuild-mlops.schema.json new file mode 100644 index 000000000..87dd96e83 --- /dev/null +++ b/tools/projmgr/schemas/cbuild-mlops.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/schemas/projmgr/2.13.0/tools/projmgr/schemas/cbuild-mlops.schema.json", + "title": "CMSIS cbuild-mlops", + "description": "contains parameters for the MLOps system", + "version": "2.13.0", + "type": "object", + "properties": { + "cbuild-mlops": { + "$ref": "./common.schema.json#/definitions/MlopsDescType" + } + }, + "additionalProperties": false, + "required": [ "cbuild-mlops" ] +} diff --git a/tools/projmgr/schemas/common.schema.json b/tools/projmgr/schemas/common.schema.json index c02f89c8c..6674d769a 100644 --- a/tools/projmgr/schemas/common.schema.json +++ b/tools/projmgr/schemas/common.schema.json @@ -1213,6 +1213,7 @@ "language-CPP": { "$ref": "#/definitions/LanguageCppType" }, "link-time-optimize": { "$ref": "#/definitions/LinkTimeOptimizeType" }, "misc": { "$ref": "#/definitions/MiscTypes" }, + "mlops": { "$ref": "#/definitions/MlopsType" }, "optimize": { "$ref": "#/definitions/OptimizeType" }, "output-dirs": { "$ref": "#/definitions/OutputDirectoriesType" }, "packs": { "$ref": "#/definitions/PacksType" }, @@ -2635,6 +2636,121 @@ }, "additionalProperties": false, "required": ["core", "max-clock"] + }, + "MlopsTargetType": { + "type": "object", + "description": "Target selection for hardware or simulator.", + "properties": { + "target": { "type": "string", "description": "Explicit [@] selection." } + }, + "additionalProperties": false + }, + "MlopsType": { + "title": "mlops:\nDocumentation: https://open-cmsis-pack.github.io/cmsis-toolbox/Experimental-Features/#mlops-management", + "description": "Configuration for MLOps features.", + "type": ["null", "object"], + "properties": { + "description": { "type": "string", "description": "Brief description of the MLOps configuration." }, + "npu": { + "type": "object", + "description": "NPU configuration.", + "properties": { + "type": { "type": "string", "description": "NPU type (default: first NPU from DFP device features)." }, + "macs": { "type": "number", "description": "Number of MACs (default: first NPU from DFP device features)." } + }, + "additionalProperties": false + }, + "vela": { + "type": "object", + "description": "Vela compiler configuration.", + "properties": { + "ini": { "type": "string", "description": "Path to Vela INI configuration file (default: use INI file from DFP)." }, + "system": { "type": "string", "description": "System configuration name from the INI file." }, + "memory": { "type": "string", "description": "Memory mode name from the INI file." }, + "misc": { "type": "string", "description": "Additional options string for Vela." } + }, + "additionalProperties": false + }, + "model": { + "type": "object", + "description": "AI model configuration.", + "properties": { + "clayer": { "type": "string", "description": "Path to the AI clayer file or variable." }, + "name": { "type": "string", "description": "Model name used as namespace (default: Algorithm)." } + }, + "additionalProperties": false + }, + "hardware": { "$ref": "#/definitions/MlopsTargetType" }, + "simulator": { "$ref": "#/definitions/MlopsTargetType" } + }, + "additionalProperties": false + }, + "MlopsDescType": { + "title": "cbuild-mlops:\nDocumentation: https://open-cmsis-pack.github.io/cmsis-toolbox/Experimental-Features/#mlops-management", + "type": ["null", "object"], + "properties": { + "description": { "type": "string", "description": "Brief description of the MLOps configuration." }, + "processor": { + "type": "object", + "description": "Processor information.", + "properties": { + "type": { "type": "string", "description": "Processor core type (e.g. Cortex-M55)." } + }, + "additionalProperties": false + }, + "npu": { + "type": "object", + "description": "NPU information.", + "properties": { + "type": { "type": "string", "description": "NPU type." }, + "macs": { "type": "number", "description": "Number of MACs." } + }, + "additionalProperties": false + }, + "vela": { + "type": "object", + "description": "Vela compiler information.", + "properties": { + "ini": { "type": "string", "description": "Path to Vela INI configuration file." }, + "options": { "type": "string", "description": "Resolved Vela command-line options string." } + }, + "additionalProperties": false + }, + "model": { + "type": "object", + "description": "AI model information.", + "properties": { + "clayer": { "type": "string", "description": "Path to the AI clayer file." }, + "name": { "type": "string", "description": "Model name used as namespace." } + }, + "additionalProperties": false + }, + "hardware": { "$ref": "#/definitions/MlopsRunDescType" }, + "simulator": { "$ref": "#/definitions/MlopsRunDescType" } + }, + "additionalProperties": false + }, + "MlopsOutputType": { + "type": "object", + "description": "Output image file.", + "properties": { + "file": { "type": "string", "description": "Path to the output file." }, + "type": { "type": "string", "description": "Output file type." } + }, + "additionalProperties": false, + "required": ["file", "type"] + }, + "MlopsRunDescType": { + "type": "object", + "description": "Run configuration.", + "properties": { + "active": { "type": "string", "description": "Active target-set in the form [@]." }, + "cbuild-run": { "type": "string", "description": "Path to the *.cbuild-run.yml file." }, + "output": { "type": "array", "description": "List of output image files.", "items": { "$ref": "#/definitions/MlopsOutputType" } }, + "model": { "type": "string", "description": "Simulator model executable (e.g. FVP_Corstone_SSE-320)." }, + "config-file": { "type": "string", "description": "Path to the simulator configuration file." } + }, + "additionalProperties": false } } } diff --git a/tools/projmgr/src/ProjMgr.cpp b/tools/projmgr/src/ProjMgr.cpp index e6b66485b..61701e574 100644 --- a/tools/projmgr/src/ProjMgr.cpp +++ b/tools/projmgr/src/ProjMgr.cpp @@ -70,6 +70,7 @@ ProjMgr::ProjMgr() : m_extGenerator(&m_parser), m_worker(&m_parser, &m_extGenerator), m_emitter(&m_parser, &m_worker), + m_mlops(&m_worker), m_rpcServer(*this), m_checkSchema(false), m_missingPacks(false), @@ -610,6 +611,17 @@ bool ProjMgr::GenerateYMLConfigurationFiles(bool previousResult) { map executes; m_worker.GetExecutes(executes); + // Generate cbuild-mlops file + if (m_parser.GetCsolution().mlops.enabled) { + MlopsType mlops; + if (!m_mlops.CollectSettings(m_parser.GetCsolution(), mlops)) { + result = false; + } + if (!m_emitter.GenerateMlops(mlops)) { + result = false; + } + } + // Generate cbuild index file if (!m_emitter.GenerateCbuildIndex(m_processedContexts, m_failedContext, executes)) { return false; diff --git a/tools/projmgr/src/ProjMgrCbuildMlops.cpp b/tools/projmgr/src/ProjMgrCbuildMlops.cpp new file mode 100644 index 000000000..665553e20 --- /dev/null +++ b/tools/projmgr/src/ProjMgrCbuildMlops.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020-2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ProductInfo.h" +#include "ProjMgrCbuildBase.h" +#include "ProjMgrUtils.h" +#include "ProjMgrYamlEmitter.h" +#include "ProjMgrYamlParser.h" +#include "RteFsUtils.h" + +using namespace std; + +// cbuild-mlops +class ProjMgrCbuildMlops : public ProjMgrCbuildBase { +public: + ProjMgrCbuildMlops(YAML::Node node, const MlopsType& mlops, const std::string& directory); +protected: + std::string m_directory; + + void SetProcessorNode(YAML::Node node, const MlopsProcessorType& processor); + void SetNpuNode(YAML::Node node, const MlopsNpuType& npu); + void SetVelaNode(YAML::Node node, const MlopsVelaType& vela); + void SetModelNode(YAML::Node node, const MlopsModelType& model); + void SetRunNode(YAML::Node node, const MlopsRunType& run); + void SetSimulatorNode(YAML::Node node, const MlopsSimulatorType& simulator); + +}; + +ProjMgrCbuildMlops::ProjMgrCbuildMlops(YAML::Node node, + const MlopsType& mlops, const std::string& directory) : m_directory(directory) { + SetNodeValue(node[YAML_DESCRIPTION], mlops.description); + SetProcessorNode(node[YAML_PROCESSOR], mlops.processor); + SetNpuNode(node[YAML_NPU], mlops.npu); + SetVelaNode(node[YAML_VELA], mlops.vela); + SetModelNode(node[YAML_MODEL], mlops.model); + SetRunNode(node[YAML_HARDWARE], mlops.hardware); + SetSimulatorNode(node[YAML_SIMULATOR], mlops.simulator); +}; + +void ProjMgrCbuildMlops::SetProcessorNode(YAML::Node node, const MlopsProcessorType& processor) { + SetNodeValue(node[YAML_TYPE], processor.type); +} + +void ProjMgrCbuildMlops::SetNpuNode(YAML::Node node, const MlopsNpuType& npu) { + SetNodeValue(node[YAML_TYPE], npu.type); + if (!npu.macs.empty()) { + node[YAML_MACS] = RteUtils::StringToULL(npu.macs); + } +} + +void ProjMgrCbuildMlops::SetVelaNode(YAML::Node node, const MlopsVelaType& vela) { + if (!vela.ini.empty()) { + SetNodeValue(node[YAML_INI], FormatPath(vela.ini, m_directory)); + } + SetNodeValue(node[YAML_OPTIONS], vela.options); +} + +void ProjMgrCbuildMlops::SetModelNode(YAML::Node node, const MlopsModelType& model) { + if (!model.clayer.empty()) { + SetNodeValue(node[YAML_CLAYER], FormatPath(model.clayer, m_directory)); + } + SetNodeValue(node[YAML_NAME], model.name); +} + +void ProjMgrCbuildMlops::SetRunNode(YAML::Node node, const MlopsRunType& run) { + SetNodeValue(node[YAML_ACTIVE], run.active); + if (!run.cbuildRun.empty()) { + SetNodeValue(node[YAML_CBUILD_RUN], FormatPath(run.cbuildRun, m_directory)); + } + for (const auto& item : run.output) { + YAML::Node outputNode; + if (!item.file.empty()) { + SetNodeValue(outputNode[YAML_FILE], FormatPath(item.file, m_directory)); + } + SetNodeValue(outputNode[YAML_TYPE], item.type); + if (outputNode.size() > 0) { + node[YAML_OUTPUT].push_back(outputNode); + } + } +} + +void ProjMgrCbuildMlops::SetSimulatorNode(YAML::Node node, const MlopsSimulatorType& simulator) { + SetRunNode(node, simulator); + SetNodeValue(node[YAML_MODEL], simulator.model); + if (!simulator.configFile.empty()) { + SetNodeValue(node[YAML_CONFIG_FILE], FormatPath(simulator.configFile, m_directory)); + } +} + +//-- ProjMgrYamlEmitter::GenerateMlops -------------------------------------------------------- +bool ProjMgrYamlEmitter::GenerateMlops(const MlopsType& mlops) { + // generate cbuild-mlops.yml + const string& filename = m_outputDir + "/" + m_parser->GetCsolution().name + ".cbuild-mlops.yml"; + const string& directory = RteFsUtils::ParentPath(filename); + + YAML::Node rootNode; + ProjMgrCbuildMlops cbuildMlops(rootNode[YAML_CBUILD_MLOPS], mlops, directory); + return WriteFile(rootNode, filename); +} diff --git a/tools/projmgr/src/ProjMgrMlops.cpp b/tools/projmgr/src/ProjMgrMlops.cpp new file mode 100644 index 000000000..aa13f4538 --- /dev/null +++ b/tools/projmgr/src/ProjMgrMlops.cpp @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ProjMgrMlops.h" + +#include "ProjMgrLogger.h" +#include "ProjMgrParser.h" +#include "ProjMgrUtils.h" + +using namespace std; + +ProjMgrMlops::ProjMgrMlops(ProjMgrWorker* worker) : m_worker(worker) { + // Reserved +} + +ProjMgrMlops::~ProjMgrMlops(void) { + // Reserved +} + +bool ProjMgrMlops::FindTargetType(const CsolutionItem& csolution, const string& typeName, TargetType& targetType) const { + for (const auto& [name, type] : csolution.targetTypes) { + if (name == typeName) { + targetType = type; + return true; + } + } + ProjMgrLogger::Get().Error("mlops: target-type '" + typeName + "' not found"); + return false; +} + +bool ProjMgrMlops::GetTargetSetItemRef(const TargetType& targetType, const string& targetTypeName, + const string& targetSetName, bool simulatorDefault, TargetSetItem& targetSet) const { + if (targetSetName.empty()) { + if (simulatorDefault) { + for (const auto& set : targetType.targetSet) { + if (set.debugger.name == "Arm-FVP") { + targetSet = set; + return true; + } + } + ProjMgrLogger::Get().Error("mlops: simulator target with debugger 'Arm-FVP' not found"); + return false; + } + if (!targetType.targetSet.empty()) { + targetSet = targetType.targetSet.front(); + return true; + } + } else { + for (const auto& set : targetType.targetSet) { + if (set.set == targetSetName) { + targetSet = set; + return true; + } + } + } + ProjMgrLogger::Get().Error("mlops: target-type '" + targetTypeName + "' target-set '" + + (targetSetName.empty() ? "" : targetSetName) + "' not found"); + return false; +} + +string ProjMgrMlops::BuildActive(const string& targetType, const string& targetSet) const { + return targetSet.empty() ? targetType : targetType + "@" + targetSet; +} + +string ProjMgrMlops::GetCustomScalar(const CustomItem& custom, const string& key) const { + for (const auto& [customKey, value] : custom.map) { + if (customKey == key) { + return value.scalar; + } + } + return RteUtils::EMPTY_STRING; +} + +string ProjMgrMlops::BuildVelaOptions(const MlopsNpuType& npu, const MlopsVelaItem& vela) const { + string options; + if (!npu.type.empty() && !npu.macs.empty()) { + options += "--accelerator-config " + RteUtils::ToLower(npu.type) + "-" + npu.macs; + } + if (!vela.system.empty()) { + options += (options.empty() ? "" : " ") + string("--system-config ") + vela.system; + } + if (!vela.memory.empty()) { + options += (options.empty() ? "" : " ") + string("--memory-mode ") + vela.memory; + } + if (!vela.misc.empty()) { + options += (options.empty() ? "" : " ") + vela.misc; + } + return options; +} + +void ProjMgrMlops::SetMlopsRunType(MlopsRunType& run, const string& targetType, const string& targetSet, + const vector& contexts, const string& outBaseDir, const string& solutionName) const { + run.active = BuildActive(targetType, targetSet); + run.cbuildRun = outBaseDir + '/' + solutionName + '+' + targetType + ".cbuild-run.yml"; + for (const auto& context : contexts) { + if (context.outputTypes.elf.on) { + MlopsOutputType output; + output.file = context.directories.cprj + '/' + context.directories.outdir + '/' + context.outputTypes.elf.filename; + output.type = RteConstants::OUTPUT_TYPE_ELF; + run.output.push_back(output); + } + } +} + +bool ProjMgrMlops::CollectSettings(const CsolutionItem& csolution, MlopsType& mlops) { + mlops = {}; + + const auto& solutionMlops = csolution.mlops; + + // hardware and simulator target types + const string hardwareType = !solutionMlops.hardware.targetType.empty() ? solutionMlops.hardware.targetType : + (!csolution.targetTypes.empty() ? csolution.targetTypes.front().first : RteUtils::EMPTY_STRING); + const string simulatorType = !solutionMlops.simulator.targetType.empty() ? solutionMlops.simulator.targetType : + (!csolution.targetTypes.empty() ? csolution.targetTypes.back().first : RteUtils::EMPTY_STRING); + + // get hardware set + TargetType hardwareTargetType; + TargetSetItem hardwareTargetSet; + if (!FindTargetType(csolution, hardwareType, hardwareTargetType) || + !GetTargetSetItemRef(hardwareTargetType, hardwareType, solutionMlops.hardware.targetSet, false, hardwareTargetSet)) { + return false; + } + + // get simulator set + TargetType simulatorTargetType; + TargetSetItem simulatorTargetSet; + if (!FindTargetType(csolution, simulatorType, simulatorTargetType) || + !GetTargetSetItemRef(simulatorTargetType, simulatorType, solutionMlops.simulator.targetSet, true, simulatorTargetSet)) { + return false; + } + + // get all context items + map* contexts = nullptr; + m_worker->GetContexts(contexts); + + // find hardware and simulator contexts + vector hardwareContexts, simulatorContexts; + StrSet pnames; + vector&>> refs = { + {hardwareTargetSet, hardwareType, hardwareContexts}, {simulatorTargetSet, simulatorType, simulatorContexts}}; + for (auto& [targetSet, targetType, ref] : refs) { + for (const auto& image : targetSet.images) { + if (!image.context.empty()) { + const string contextName = image.context + "+" + targetType; + if (contexts->find(contextName) != contexts->end()) { + // process context precedences if needed + auto& context = contexts->at(contextName); + if (!context.precedences) { + if (!m_worker->ParseContextLayers(context) || !m_worker->LoadPacks(context) || + !m_worker->ProcessPrecedences(context, BoardOrDevice::Both) || + !m_worker->SetTargetAttributes(context, context.targetAttributes)) { + return false; + } + m_worker->CollectNpuInfo(context); + } + ref.push_back(context); + pnames.insert(context.deviceItem.pname); + } + } + } + } + + // check if hardware and simulator contexts were found + vector&, const string&, const string&>> contextRefs = { + {hardwareContexts, hardwareType, hardwareTargetSet.set}, + {simulatorContexts, simulatorType, simulatorTargetSet.set} + }; + for (const auto& [ref, targetType, targetSet] : contextRefs) { + if (ref.empty()) { + ProjMgrLogger::Get().Error("mlops: target-type '" + targetType + "' target-set '" + + (targetSet.empty() ? "" : targetSet) + "' project-contexts not found"); + return false; + } + } + + auto& hardwareContext = hardwareContexts.front(); + auto& simulatorContext = simulatorContexts.front(); + + // mlops description + mlops.description = solutionMlops.description; + + // get hardware processor type ("Dcore") + if (hardwareContext.targetAttributes.find("Dcore") != hardwareContext.targetAttributes.end()) { + mlops.processor.type = hardwareContext.targetAttributes.at("Dcore"); + } + + // npu type and macs + mlops.npu.type = solutionMlops.npu.type; + mlops.npu.macs = solutionMlops.npu.macs; + + // filter npu info items + vector npuInfoItems; + for (NpuInfoItem npu : hardwareContext.npuInfoItems) { + if (pnames.find(npu.pname) != pnames.end()) { + npu.macs = RteUtils::StripSuffix(npu.macs, "MACs"); + npuInfoItems.push_back(npu); + } + } + + if (!npuInfoItems.empty()) { + if (mlops.npu.type.empty() && mlops.npu.macs.empty()) { + // default npu type and macs + mlops.npu.type = npuInfoItems.front().type; + mlops.npu.macs = npuInfoItems.front().macs; + } else if (!mlops.npu.type.empty() && mlops.npu.macs.empty()) { + // explicit type, find matching macs + for (const auto& npu : npuInfoItems) { + if (npu.type == mlops.npu.type) { + mlops.npu.macs = npu.macs; + break; + } + } + } else if (mlops.npu.type.empty() && !mlops.npu.macs.empty()) { + // explicit macs, find matching type + for (const auto& npu : npuInfoItems) { + if (RteUtils::StringToULL(npu.macs) == RteUtils::StringToULL(mlops.npu.macs)) { + mlops.npu.type = npu.type; + break; + } + } + } + } + + // vela options + mlops.vela.options = BuildVelaOptions(mlops.npu, solutionMlops.vela); + + // vela ini + if (solutionMlops.vela.ini.empty()) { + // default vela ini from DFP for matching NPU + for (const auto& npu : npuInfoItems) { + if (npu.type == mlops.npu.type && + RteUtils::StringToULL(npu.macs) == RteUtils::StringToULL(mlops.npu.macs)) { + mlops.vela.ini = fs::path(npu.velaAbsolutePath).filename().generic_string(); + RteFsUtils::NormalizePath(mlops.vela.ini, csolution.directory + "/.cmsis/"); + // copy file to .cmsis if it does not exist yet + if (!RteFsUtils::Exists(mlops.vela.ini)) { + RteFsUtils::CopyCheckFile(npu.velaAbsolutePath, mlops.vela.ini, false); + } + } + } + } else { + // explicit vela ini + mlops.vela.ini = solutionMlops.vela.ini; + if (!m_worker->ProcessSequenceRelative(hardwareContext, mlops.vela.ini, csolution.directory, false)) { + return false; + } + if (RteFsUtils::IsRelative(mlops.vela.ini)) { + RteFsUtils::NormalizePath(mlops.vela.ini, hardwareContext.directories.cprj); + } + } + + // model name and clayer + if (!solutionMlops.model.clayer.empty()) { + mlops.model.name = solutionMlops.model.name.empty() ? "Algorithm" : solutionMlops.model.name; + mlops.model.clayer = solutionMlops.model.clayer; + if (!m_worker->ProcessSequenceRelative(hardwareContext, mlops.model.clayer, csolution.directory, false)) { + return false; + } + if (RteFsUtils::IsRelative(mlops.model.clayer)) { + RteFsUtils::NormalizePath(mlops.model.clayer, hardwareContext.directories.cprj); + } + } + + // set hardware and simulator run types + const string outBaseDir = hardwareContext.directories.cprj + "/" + hardwareContext.directories.outBaseDir; + SetMlopsRunType(mlops.hardware, hardwareType, hardwareTargetSet.set, hardwareContexts, outBaseDir, csolution.name); + SetMlopsRunType(mlops.simulator, simulatorType, simulatorTargetSet.set, simulatorContexts, outBaseDir, csolution.name); + + // get debugger model and config-file + mlops.simulator.model = GetCustomScalar(simulatorTargetSet.debugger.custom, "model"); + mlops.simulator.configFile = GetCustomScalar(simulatorTargetSet.debugger.custom, "config-file"); + if (!mlops.simulator.configFile.empty()) { + if (!m_worker->ProcessSequenceRelative(simulatorContext, mlops.simulator.configFile, csolution.directory, false)) { + return false; + } + if (RteFsUtils::IsRelative(mlops.simulator.configFile)) { + RteFsUtils::NormalizePath(mlops.simulator.configFile, simulatorContext.directories.cprj); + } + } + + return true; +} diff --git a/tools/projmgr/src/ProjMgrYamlParser.cpp b/tools/projmgr/src/ProjMgrYamlParser.cpp index b8287766b..2a052053f 100644 --- a/tools/projmgr/src/ProjMgrYamlParser.cpp +++ b/tools/projmgr/src/ProjMgrYamlParser.cpp @@ -106,6 +106,7 @@ bool ProjMgrYamlParser::ParseCsolution(const string& input, csolution.enableCdefault = solutionNode[YAML_CDEFAULT].IsDefined(); ParseGenerators(solutionNode, csolution.path, csolution.generators); ParseExecutes(solutionNode, csolution.path, csolution.executes); + ParseMlops(solutionNode, csolution.path, csolution.mlops); } catch (YAML::Exception& e) { ProjMgrLogger::Get().Error(e.msg, "", input, e.mark.line + 1, e.mark.column + 1); @@ -1099,6 +1100,45 @@ bool ProjMgrYamlParser::ParseTargetType(const YAML::Node& parent, const string& return ParseBuildType(parent, file, targetType.build); } +void ProjMgrYamlParser::ParseMlops(const YAML::Node& parent, const string& file, MlopsItem& mlops) { + if (parent[YAML_MLOPS].IsDefined()) { + const YAML::Node& mlopsNode = parent[YAML_MLOPS]; + mlops.enabled = true; + ParseString(mlopsNode, YAML_DESCRIPTION, mlops.description); + if (mlopsNode[YAML_NPU].IsDefined()) { + const YAML::Node& npuNode = mlopsNode[YAML_NPU]; + ParseString(npuNode, YAML_TYPE, mlops.npu.type); + ParseNumber(npuNode, file, YAML_MACS, mlops.npu.macs); + } + if (mlopsNode[YAML_VELA].IsDefined()) { + const YAML::Node& velaNode = mlopsNode[YAML_VELA]; + ParsePortablePath(velaNode, file, YAML_INI, mlops.vela.ini); + ParseString(velaNode, YAML_SYSTEM, mlops.vela.system); + ParseString(velaNode, YAML_MEMORY, mlops.vela.memory); + ParseString(velaNode, YAML_MISC, mlops.vela.misc); + } + if (mlopsNode[YAML_MODEL].IsDefined()) { + const YAML::Node& modelNode = mlopsNode[YAML_MODEL]; + ParsePortablePath(modelNode, file, YAML_CLAYER, mlops.model.clayer); + ParseString(modelNode, YAML_NAME, mlops.model.name); + } + if (mlopsNode[YAML_HARDWARE].IsDefined()) { + const YAML::Node& hardwareNode = mlopsNode[YAML_HARDWARE]; + string hardwareTarget; + ParseString(hardwareNode, YAML_TARGET, hardwareTarget); + mlops.hardware.targetType = RteUtils::GetPrefix(hardwareTarget, '@'); + mlops.hardware.targetSet = RteUtils::GetSuffix(hardwareTarget, '@'); + } + if (mlopsNode[YAML_SIMULATOR].IsDefined()) { + const YAML::Node& simulatorNode = mlopsNode[YAML_SIMULATOR]; + string simulatorTarget; + ParseString(simulatorNode, YAML_TARGET, simulatorTarget); + mlops.simulator.targetType = RteUtils::GetPrefix(simulatorTarget, '@'); + mlops.simulator.targetSet = RteUtils::GetSuffix(simulatorTarget, '@'); + } + } +} + void ProjMgrYamlParser::ParseTargetSet(const YAML::Node& parent, const string& file, std::vector& targetSets) { if (parent[YAML_TARGET_SET].IsDefined()) { const YAML::Node& targetSetNode = parent[YAML_TARGET_SET]; @@ -1195,6 +1235,7 @@ const set solutionKeys = { YAML_CDEFAULT, YAML_GENERATORS, YAML_EXECUTES, + YAML_MLOPS, }; const set projectsKeys = { diff --git a/tools/projmgr/src/ProjMgrYamlSchemaChecker.cpp b/tools/projmgr/src/ProjMgrYamlSchemaChecker.cpp index 91d20ac75..aeb4dbb51 100644 --- a/tools/projmgr/src/ProjMgrYamlSchemaChecker.cpp +++ b/tools/projmgr/src/ProjMgrYamlSchemaChecker.cpp @@ -19,7 +19,7 @@ bool ProjMgrYamlSchemaChecker::Validate(const std::string& file) { // Check if the input file exist if (!RteFsUtils::Exists(file)) { - ProjMgrLogger::Get().Error("file doesn't exist", "", file); + ProjMgrLogger::Get().Error("file doesn't exist: '" + file + "'"); return false; } diff --git a/tools/projmgr/test/data/MLOps/ai_layer/ai_layer.clayer.yml b/tools/projmgr/test/data/MLOps/ai_layer/ai_layer.clayer.yml new file mode 100644 index 000000000..650ec35ae --- /dev/null +++ b/tools/projmgr/test/data/MLOps/ai_layer/ai_layer.clayer.yml @@ -0,0 +1,3 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/clayer.schema.json + +layer: diff --git a/tools/projmgr/test/data/MLOps/core0/core0.cproject.yml b/tools/projmgr/test/data/MLOps/core0/core0.cproject.yml new file mode 100644 index 000000000..cc3854288 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/core0/core0.cproject.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/cproject.schema.json + +project: + + device: :cm0_core0 diff --git a/tools/projmgr/test/data/MLOps/core1/core1.cproject.yml b/tools/projmgr/test/data/MLOps/core1/core1.cproject.yml new file mode 100644 index 000000000..33cb5d558 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/core1/core1.cproject.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/cproject.schema.json + +project: + + device: :cm0_core1 diff --git a/tools/projmgr/test/data/MLOps/extended.csolution.yml b/tools/projmgr/test/data/MLOps/extended.csolution.yml new file mode 100644 index 000000000..2be151585 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/extended.csolution.yml @@ -0,0 +1,53 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: # enable *.cbuild-mlops.yml + description: ML model with extended configuration + npu: + type: Ethos-U85 # specify NPU (default: first NPU from DFP device features) + macs: 256 # specify MACs (default: first NPU from DFP device features) + vela: + ini: vela/custom.ini # explicit INI file (default: use INI file from DFP) + system: System_Config # system configuration from INI file + memory: Memory_Mode # memory configuration from INI file + misc: --extra-options # string with additional options for Vela + model: + clayer: $AI-Layer$ # path to layer or variable + name: Algorithm # optional model name (default Algorithm), serves as namespace + hardware: # hardware target for testing + target: Hardware # explicit [@] (default: first target-type, first set) + simulator: # simulator target for testing + target: Simulator@FVP-Test # explicit [@] (default: last target-type, first set, check if debugger name: Arm-FVP) + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + variables: + - AI-Layer: $SolutionDir()$/ai_layer/ai_layer.clayer.yml + target-set: + - set: + images: + - project-context: core0 + - project-context: core1 + + - type: Simulator + device: RteTest_ARMCM0_Dual + define: + - SIMULATOR + variables: + - AI-Layer: $SolutionDir()$/ai_layer/ai_layer.clayer.yml + target-set: + - set: FVP-Test + debugger: + name: Arm-FVP + model: FVP_Corstone_SSE-320 + config-file: fvp/fvp_config.txt + images: + - project-context: core0 + - project-context: core1 + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/failure1.csolution.yml b/tools/projmgr/test/data/MLOps/failure1.csolution.yml new file mode 100644 index 000000000..56489ecf4 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/failure1.csolution.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: + hardware: + target: OtherHardware + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + - type: Simulator + device: RteTest_ARMCM0_Dual + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/failure2.csolution.yml b/tools/projmgr/test/data/MLOps/failure2.csolution.yml new file mode 100644 index 000000000..7a8665f63 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/failure2.csolution.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: + simulator: + target: OtherSimulator + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + - type: Simulator + device: RteTest_ARMCM0_Dual + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/failure3.csolution.yml b/tools/projmgr/test/data/MLOps/failure3.csolution.yml new file mode 100644 index 000000000..0fd62d7b0 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/failure3.csolution.yml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + target-set: + - set: + - type: Simulator + device: RteTest_ARMCM0_Dual + target-set: + - set: FVP-Test + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/failure4.csolution.yml b/tools/projmgr/test/data/MLOps/failure4.csolution.yml new file mode 100644 index 000000000..bda135282 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/failure4.csolution.yml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + target-set: + - set: + images: + - project-context: core0 + - project-context: core1 + + - type: Simulator + device: RteTest_ARMCM0_Dual + target-set: + - set: FVP-Test + debugger: + name: Arm-FVP + images: + - project-context: wrong_context + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/fvp/fvp_config.txt b/tools/projmgr/test/data/MLOps/fvp/fvp_config.txt new file mode 100644 index 000000000..5859824e9 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/fvp/fvp_config.txt @@ -0,0 +1 @@ +// Test purposes diff --git a/tools/projmgr/test/data/MLOps/minimal.csolution.yml b/tools/projmgr/test/data/MLOps/minimal.csolution.yml new file mode 100644 index 000000000..1540861bb --- /dev/null +++ b/tools/projmgr/test/data/MLOps/minimal.csolution.yml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: + description: ML model with minimal configuration + model: + clayer: $AI-Layer$ + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + variables: + - AI-Layer: $SolutionDir()$/ai_layer/ai_layer.clayer.yml + target-set: + - set: + images: + - project-context: core0 + - project-context: core1 + + - type: Simulator + device: RteTest_ARMCM0_Dual + define: + - SIMULATOR + variables: + - AI-Layer: $SolutionDir()$/ai_layer/ai_layer.clayer.yml + target-set: + - set: FVP-Test + debugger: + name: Arm-FVP + model: FVP_Corstone_SSE-320 + config-file: fvp/fvp_config.txt + images: + - project-context: core0 + - project-context: core1 + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/npu_macs_only.csolution.yml b/tools/projmgr/test/data/MLOps/npu_macs_only.csolution.yml new file mode 100644 index 000000000..35d35dfe7 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/npu_macs_only.csolution.yml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: + description: infer type from explicit npu macs + npu: + macs: 128 + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + target-set: + - set: + images: + - project-context: core0 + - project-context: core1 + + - type: Simulator + device: RteTest_ARMCM0_Dual + target-set: + - set: FVP-Test + debugger: + name: Arm-FVP + images: + - project-context: core0 + - project-context: core1 + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/npu_type_only.csolution.yml b/tools/projmgr/test/data/MLOps/npu_type_only.csolution.yml new file mode 100644 index 000000000..4fc6746e4 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/npu_type_only.csolution.yml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Open-CMSIS-Pack/devtools/main/tools/projmgr/schemas/csolution.schema.json + +solution: + compiler: AC6 + + mlops: + description: infer macs from explicit npu type + npu: + type: Ethos-U55 + + target-types: + - type: Hardware + device: RteTest_ARMCM0_Dual + target-set: + - set: + images: + - project-context: core0 + - project-context: core1 + + - type: Simulator + device: RteTest_ARMCM0_Dual + target-set: + - set: FVP-Test + debugger: + name: Arm-FVP + images: + - project-context: core0 + - project-context: core1 + + projects: + - project: core0/core0.cproject.yml + - project: core1/core1.cproject.yml diff --git a/tools/projmgr/test/data/MLOps/ref/extended.cbuild-mlops.yml b/tools/projmgr/test/data/MLOps/ref/extended.cbuild-mlops.yml new file mode 100644 index 000000000..73dc97620 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/ref/extended.cbuild-mlops.yml @@ -0,0 +1,31 @@ +cbuild-mlops: + description: ML model with extended configuration + processor: + type: Cortex-M0 + npu: + type: Ethos-U85 + macs: 256 + vela: + ini: vela/custom.ini + options: --accelerator-config ethos-u85-256 --system-config System_Config --memory-mode Memory_Mode --extra-options + model: + clayer: ai_layer/ai_layer.clayer.yml + name: Algorithm + hardware: + active: Hardware + cbuild-run: out/extended+Hardware.cbuild-run.yml + output: + - file: out/core0/Hardware/core0.axf + type: elf + - file: out/core1/Hardware/core1.axf + type: elf + simulator: + active: Simulator@FVP-Test + cbuild-run: out/extended+Simulator.cbuild-run.yml + output: + - file: out/core0/Simulator/core0.axf + type: elf + - file: out/core1/Simulator/core1.axf + type: elf + model: FVP_Corstone_SSE-320 + config-file: fvp/fvp_config.txt diff --git a/tools/projmgr/test/data/MLOps/ref/minimal.cbuild-mlops.yml b/tools/projmgr/test/data/MLOps/ref/minimal.cbuild-mlops.yml new file mode 100644 index 000000000..2bec0bc41 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/ref/minimal.cbuild-mlops.yml @@ -0,0 +1,31 @@ +cbuild-mlops: + description: ML model with minimal configuration + processor: + type: Cortex-M0 + npu: + type: Ethos-U55 + macs: 128 + vela: + ini: .cmsis/vela_deviceLevel.ini + options: --accelerator-config ethos-u55-128 + model: + clayer: ai_layer/ai_layer.clayer.yml + name: Algorithm + hardware: + active: Hardware + cbuild-run: out/minimal+Hardware.cbuild-run.yml + output: + - file: out/core0/Hardware/core0.axf + type: elf + - file: out/core1/Hardware/core1.axf + type: elf + simulator: + active: Simulator@FVP-Test + cbuild-run: out/minimal+Simulator.cbuild-run.yml + output: + - file: out/core0/Simulator/core0.axf + type: elf + - file: out/core1/Simulator/core1.axf + type: elf + model: FVP_Corstone_SSE-320 + config-file: fvp/fvp_config.txt diff --git a/tools/projmgr/test/data/MLOps/vela/custom.ini b/tools/projmgr/test/data/MLOps/vela/custom.ini new file mode 100644 index 000000000..5859824e9 --- /dev/null +++ b/tools/projmgr/test/data/MLOps/vela/custom.ini @@ -0,0 +1 @@ +// Test purposes diff --git a/tools/projmgr/test/src/ProjMgrUnitTests.cpp b/tools/projmgr/test/src/ProjMgrUnitTests.cpp index 8eae33b10..e68e8fa2f 100644 --- a/tools/projmgr/test/src/ProjMgrUnitTests.cpp +++ b/tools/projmgr/test/src/ProjMgrUnitTests.cpp @@ -7708,3 +7708,69 @@ TEST_F(ProjMgrUnitTests, ParseCommandLine_MutualExclusionOptions) { auto errStr = streamRedirect.GetErrorString(); EXPECT_NE(string::npos, errStr.find("error csolution: command line options '--quiet' and '--verbose' are mutually exclusive")); } + +TEST_F(ProjMgrUnitTests, GenerateMLOps) { + char* argv[5]; + string csolution = testinput_folder + "/MLOps/minimal.csolution.yml"; + argv[1] = (char*)"convert"; + argv[2] = (char*)csolution.c_str(); + argv[3] = (char*)"--active"; + argv[4] = (char*)"Simulator"; + EXPECT_EQ(0, RunProjMgr(5, argv, m_envp)); + ProjMgrTestEnv::CompareFile(testinput_folder + "/MLOps/minimal.cbuild-mlops.yml", + testinput_folder + "/MLOps/ref/minimal.cbuild-mlops.yml"); + + // different active target should not change mlops + argv[4] = (char*)""; + EXPECT_EQ(0, RunProjMgr(5, argv, m_envp)); + ProjMgrTestEnv::CompareFile(testinput_folder + "/MLOps/minimal.cbuild-mlops.yml", + testinput_folder + "/MLOps/ref/minimal.cbuild-mlops.yml"); + + // extended mlops configuration + csolution = testinput_folder + "/MLOps/extended.csolution.yml"; + argv[2] = (char*)csolution.c_str(); + EXPECT_EQ(0, RunProjMgr(5, argv, m_envp)); + ProjMgrTestEnv::CompareFile(testinput_folder + "/MLOps/extended.cbuild-mlops.yml", + testinput_folder + "/MLOps/ref/extended.cbuild-mlops.yml"); + + // infer macs from explicit npu type + csolution = testinput_folder + "/MLOps/npu_type_only.csolution.yml"; + argv[2] = (char*)csolution.c_str(); + EXPECT_EQ(0, RunProjMgr(5, argv, m_envp)); + const YAML::Node& mlopsTypeOnly = YAML::LoadFile(testinput_folder + "/MLOps/npu_type_only.cbuild-mlops.yml"); + EXPECT_EQ("Ethos-U55", mlopsTypeOnly["cbuild-mlops"]["npu"]["type"].as()); + EXPECT_EQ("128", mlopsTypeOnly["cbuild-mlops"]["npu"]["macs"].as()); + + // infer type from explicit npu macs + csolution = testinput_folder + "/MLOps/npu_macs_only.csolution.yml"; + argv[2] = (char*)csolution.c_str(); + EXPECT_EQ(0, RunProjMgr(5, argv, m_envp)); + const YAML::Node& mlopsMacsOnly = YAML::LoadFile(testinput_folder + "/MLOps/npu_macs_only.cbuild-mlops.yml"); + EXPECT_EQ("Ethos-U55", mlopsMacsOnly["cbuild-mlops"]["npu"]["type"].as()); + EXPECT_EQ("128", mlopsMacsOnly["cbuild-mlops"]["npu"]["macs"].as()); + + // test error cases: {csolution file, expected error message} + const vector> failureCases = { + { "failure1", "mlops: target-type 'OtherHardware' not found" }, + { "failure2", "mlops: target-type 'Hardware' target-set '' not found" }, + { "failure3", "mlops: simulator target with debugger 'Arm-FVP' not found" }, + { "failure4", "mlops: target-type 'Simulator' target-set 'FVP-Test' project-contexts not found" }, + }; + for (const auto& [name, expectedError] : failureCases) { + csolution = testinput_folder + "/MLOps/" + name + ".csolution.yml"; + argv[2] = (char*)csolution.c_str(); + EXPECT_EQ(1, RunProjMgr(5, argv, m_envp)); + const YAML::Node& cbuildIdx = YAML::LoadFile(testinput_folder + "/MLOps/" + name + ".cbuild-idx.yml"); + EXPECT_EQ(expectedError, cbuildIdx["build-idx"]["cbuilds"][0]["messages"]["errors"][0].as()); + } + + // file cannot be written + const string mlopsOutput = testinput_folder + "/MLOps/failure4.cbuild-mlops.yml"; + RteFsUtils::RemoveFile(mlopsOutput); + EXPECT_TRUE(RteFsUtils::CreateDirectories(mlopsOutput)); + EXPECT_EQ(1, RunProjMgr(5, argv, m_envp)); + const YAML::Node& cbuildIdx = YAML::LoadFile(testinput_folder + "/MLOps/failure4.cbuild-idx.yml"); + EXPECT_EQ("failure4.cbuild-mlops.yml - file cannot be written", + cbuildIdx["build-idx"]["cbuilds"][0]["messages"]["errors"][1].as()); + RteFsUtils::RemoveDir(mlopsOutput); +} From 0c81317048479847e87b0e7439ea6344baad9e3e Mon Sep 17 00:00:00 2001 From: Daniel Brondani Date: Fri, 8 May 2026 11:48:26 +0200 Subject: [PATCH 2/3] Fix indentations --- tools/projmgr/include/ProjMgrWorker.h | 2 +- tools/projmgr/include/ProjMgrYamlParser.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/projmgr/include/ProjMgrWorker.h b/tools/projmgr/include/ProjMgrWorker.h index 96db82aaf..566bccf47 100644 --- a/tools/projmgr/include/ProjMgrWorker.h +++ b/tools/projmgr/include/ProjMgrWorker.h @@ -1314,7 +1314,7 @@ class ProjMgrWorker { bool ProcessSequencesRelatives(ContextItem& context, bool rerun); bool ProcessSequencesRelatives(ContextItem& context, std::vector& src, const std::string& ref = std::string(), std::string outDir = std::string(), bool withHeadingDot = false, bool solutionLevel = false); bool ProcessSequencesRelatives(ContextItem& context, BuildType& build, const std::string& ref = std::string()); - bool ProcessOutputFilenames(ContextItem& context); + bool ProcessOutputFilenames(ContextItem& context); bool ProcessLinkerOptions(ContextItem& context); bool ProcessLinkerOptions(ContextItem& context, const LinkerItem& linker, const std::string& ref); bool ProcessProcessorOptions(ContextItem& context); diff --git a/tools/projmgr/include/ProjMgrYamlParser.h b/tools/projmgr/include/ProjMgrYamlParser.h index 9e6c4d69f..78e4156e2 100644 --- a/tools/projmgr/include/ProjMgrYamlParser.h +++ b/tools/projmgr/include/ProjMgrYamlParser.h @@ -263,7 +263,7 @@ static constexpr const char* YAML_SYSTEM_RESOURCES = "system-resources"; static constexpr const char* YAML_SYSTEMVIEW = "systemview"; static constexpr const char* YAML_TAPINDEX = "tapindex"; static constexpr const char* YAML_TARGET = "target"; - static constexpr const char* YAML_TARGET_CONFIGURATIONS = "target-configurations"; +static constexpr const char* YAML_TARGET_CONFIGURATIONS = "target-configurations"; static constexpr const char* YAML_TARGETSEL = "targetsel"; static constexpr const char* YAML_TARGET_SET = "target-set"; static constexpr const char* YAML_TARGETTYPE = "target-type"; From b215c49fc49a3c1ee919287aee5cc5c2388a9d60 Mon Sep 17 00:00:00 2001 From: Daniel Brondani Date: Fri, 8 May 2026 16:29:51 +0200 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Sourabh Mehta <73165318+soumeh01@users.noreply.github.com> --- tools/projmgr/include/ProjMgrMlops.h | 2 +- tools/projmgr/src/ProjMgrCbuildMlops.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/projmgr/include/ProjMgrMlops.h b/tools/projmgr/include/ProjMgrMlops.h index bf7193150..23d3261dd 100644 --- a/tools/projmgr/include/ProjMgrMlops.h +++ b/tools/projmgr/include/ProjMgrMlops.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2026 Arm Limited. All rights reserved. + * Copyright (c) 2026 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/tools/projmgr/src/ProjMgrCbuildMlops.cpp b/tools/projmgr/src/ProjMgrCbuildMlops.cpp index 665553e20..5968a4778 100644 --- a/tools/projmgr/src/ProjMgrCbuildMlops.cpp +++ b/tools/projmgr/src/ProjMgrCbuildMlops.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2026 Arm Limited. All rights reserved. + * Copyright (c) 2026 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 */