Skip to content
Open
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
1 change: 1 addition & 0 deletions sdk_v2/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ set(FOUNDRY_LOCAL_SOURCES
src/utils.cc
src/util/file_lock.cc
src/http/http_download.cc
src/util/model_layout.cc
src/util/path_safety.cc
src/util/region_fallback.cc
src/util/sha256.cc
Expand Down
68 changes: 57 additions & 11 deletions sdk_v2/cpp/src/catalog/azure_catalog_models.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,44 @@ int64_t ParseIso8601ToUnix(const std::string& iso_str) {
return t == static_cast<time_t>(-1) ? 0 : static_cast<int64_t>(t);
}

DeviceType ParseDeviceType(const std::string& device) {
const auto lower = ToLower(device);
if (lower == "cpu") {
return DeviceType::kCPU;
std::optional<ModelVariantMetadata> ToModelVariantMetadata(const VariantMetadata& variant_metadata) {
if (!variant_metadata.model_format.has_value() && !variant_metadata.model_package.has_value()) {
return std::nullopt;
}

if (lower == "gpu") {
return DeviceType::kGPU;
}
ModelVariantMetadata metadata;
metadata.model_format = variant_metadata.model_format;

if (variant_metadata.model_package.has_value()) {
ModelPackageMetadata package_metadata;
package_metadata.schema_version = variant_metadata.model_package->schema_version;
package_metadata.variants.reserve(variant_metadata.model_package->variants.size());

for (const auto& catalog_variant : variant_metadata.model_package->variants) {
ModelPackageVariant package_variant;
if (catalog_variant.name.has_value()) {
package_variant.name = *catalog_variant.name;
}

if (catalog_variant.execution_provider.has_value()) {
package_variant.execution_provider = *catalog_variant.execution_provider;
}

if (catalog_variant.device.has_value()) {
package_variant.device_type = DeviceTypeFromString(*catalog_variant.device);
}

if (catalog_variant.compatibility_string.has_value()) {
package_variant.compatibility_string = *catalog_variant.compatibility_string;
}

package_metadata.variants.push_back(std::move(package_variant));
}

if (lower == "npu") {
return DeviceType::kNPU;
metadata.model_package = std::move(package_metadata);
}
return DeviceType::kNotSet;

return metadata;
}

} // anonymous namespace
Expand Down Expand Up @@ -120,11 +144,31 @@ void to_json(nlohmann::json& j, const AzureCatalogRequest& r) {
// Response deserialization (from_json)
// ========================================================================

void from_json(const nlohmann::json& j, CatalogModelPackageVariant& v) {
opt_str(j, "name", v.name);
opt_str(j, "executionProvider", v.execution_provider);
opt_str(j, "device", v.device);
opt_str(j, "compatibilityString", v.compatibility_string);
}

void from_json(const nlohmann::json& j, CatalogModelPackageMetadata& p) {
opt_int(j, "schemaVersion", p.schema_version);

if (j.contains("variants") && j["variants"].is_array()) {
p.variants = j["variants"].get<std::vector<CatalogModelPackageVariant>>();
}
}

void from_json(const nlohmann::json& j, VariantMetadata& v) {
opt_str(j, "modelType", v.model_type);
opt_str(j, "device", v.device);
opt_str(j, "executionProvider", v.execution_provider);
opt_int64(j, "fileSizeBytes", v.file_size_bytes);
opt_str(j, "modelFormat", v.model_format);

if (j.contains("modelPackage") && j["modelPackage"].is_object()) {
v.model_package = j["modelPackage"].get<CatalogModelPackageMetadata>();
}
}

void from_json(const nlohmann::json& j, VariantParent& v) {
Expand Down Expand Up @@ -286,13 +330,15 @@ std::optional<ModelInfo> CatalogModelToModelInfo(const CatalogLocalModel& cm) {
if (props.variant_info && props.variant_info->variant_metadata) {
const auto& vm = *props.variant_info->variant_metadata;
if (vm.device) {
info.device_type = ParseDeviceType(*vm.device);
info.device_type = DeviceTypeFromString(*vm.device);
}

if (vm.execution_provider) {
info.execution_provider = *vm.execution_provider;
}

info.variant_metadata = ToModelVariantMetadata(vm);

// ModelType — defaults to "ONNX" (matches C# ToAzureFoundryLocalModel)
info.string_properties[FOUNDRY_LOCAL_MODEL_PROP_MODEL_TYPE_STR] =
vm.model_type.value_or("ONNX");
Expand Down
16 changes: 16 additions & 0 deletions sdk_v2/cpp/src/catalog/azure_catalog_models.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,26 @@ struct AzureCatalogRequest {

// --- Response types ---

struct CatalogModelPackageVariant {
std::optional<std::string> name;
std::optional<std::string> execution_provider;
std::optional<std::string> device;
std::optional<std::string> compatibility_string;
};

struct CatalogModelPackageMetadata {
std::optional<int> schema_version;
std::vector<CatalogModelPackageVariant> variants;
};

/// Variant metadata nested inside Properties → VariantInfo.
struct VariantMetadata {
std::optional<std::string> model_type;
std::optional<std::string> device;
std::optional<std::string> execution_provider;
std::optional<int64_t> file_size_bytes;
std::optional<std::string> model_format;
std::optional<CatalogModelPackageMetadata> model_package;
};

struct VariantParent {
Expand Down Expand Up @@ -148,6 +162,8 @@ void to_json(nlohmann::json& j, const AzureCatalogRequest& r);

// --- Response deserialization (from_json) ---

void from_json(const nlohmann::json& j, CatalogModelPackageVariant& v);
void from_json(const nlohmann::json& j, CatalogModelPackageMetadata& p);
void from_json(const nlohmann::json& j, VariantMetadata& v);
void from_json(const nlohmann::json& j, VariantParent& v);
void from_json(const nlohmann::json& j, VariantInfo& v);
Expand Down
130 changes: 130 additions & 0 deletions sdk_v2/cpp/src/catalog/azure_model_catalog.cc
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,133 @@ std::vector<ModelInfo> DeduplicateByModelId(std::vector<ModelInfo> model_infos)
return deduplicated;
}

bool IsCompatible(CompiledModelCompatibility compatibility) {
return compatibility == CompiledModelCompatibility::kSupportedOptimal ||
compatibility == CompiledModelCompatibility::kSupportedPreferRecompilation;
}

bool IsRelevantPackageVariant(const ModelInfo& info, const ModelPackageVariant& package_variant) {
if (!info.execution_provider.empty() && !package_variant.execution_provider.empty() &&
info.execution_provider != package_variant.execution_provider) {
return false;
}

if (info.device_type != DeviceType::kNotSet &&
package_variant.device_type != DeviceType::kNotSet &&
info.device_type != package_variant.device_type) {
return false;
}

return true;
}

std::optional<DeviceType> GetDeviceConstraint(const ModelInfo& info,
const ModelPackageVariant& package_variant) {
if (package_variant.device_type != DeviceType::kNotSet) {
return package_variant.device_type;
}

if (info.device_type != DeviceType::kNotSet) {
return info.device_type;
}

return std::nullopt;
}

std::string_view GetRequestedExecutionProvider(const ModelInfo& info,
const ModelPackageVariant& package_variant) {
if (!package_variant.execution_provider.empty()) {
return package_variant.execution_provider;
}

return info.execution_provider;
}

bool ShouldExposeModelInfo(const ModelInfo& info,
const IEpDetector& ep_detector,
ILogger& logger) {
if (!info.variant_metadata.has_value() ||
!info.variant_metadata->model_package.has_value() ||
info.variant_metadata->model_package->variants.empty()) {
return true;
}

bool saw_relevant_variant = false;
bool saw_unknown_compatibility = false;

for (const auto& package_variant : info.variant_metadata->model_package->variants) {
if (!IsRelevantPackageVariant(info, package_variant)) {
continue;
}

saw_relevant_variant = true;

const auto execution_provider = GetRequestedExecutionProvider(info, package_variant);
if (execution_provider.empty() || package_variant.compatibility_string.empty()) {
logger.Log(LogLevel::Debug,
fmt::format("Keeping catalog model '{}' because package variant '{}' has incomplete "
"compatibility metadata.",
info.model_id,
package_variant.name));
saw_unknown_compatibility = true;
continue;
}

const auto compatibility = ep_detector.GetModelCompatibilityForEpDevices(
execution_provider,
GetDeviceConstraint(info, package_variant),
package_variant.compatibility_string);

if (IsCompatible(compatibility)) {
return true;
}

if (compatibility == CompiledModelCompatibility::kUnknown) {
logger.Log(LogLevel::Debug,
fmt::format("Keeping catalog model '{}' because compatibility is unknown for package "
"variant '{}'.",
info.model_id,
package_variant.name));
saw_unknown_compatibility = true;
}
}

if (!saw_relevant_variant) {
logger.Log(LogLevel::Debug,
fmt::format("Keeping catalog model '{}' because no package variant definitively matches "
"its catalog EP/device.",
info.model_id));
return true;
}

if (saw_unknown_compatibility) {
return true;
}

logger.Log(LogLevel::Debug,
fmt::format("Filtering catalog model '{}' because all relevant ORT model package variants "
"are unsupported.",
info.model_id));
return false;
}

/// Drops catalog entries whose ORT model package variants are all known to be unsupported
/// on the current hardware. Entries without package metadata are always kept.
std::vector<ModelInfo> FilterVisibleInfos(std::vector<ModelInfo> model_infos,
const IEpDetector& ep_detector,
ILogger& logger) {
std::vector<ModelInfo> visible_infos;
visible_infos.reserve(model_infos.size());

for (auto& info : model_infos) {
if (ShouldExposeModelInfo(info, ep_detector, logger)) {
visible_infos.push_back(std::move(info));
}
}

return visible_infos;
}

void RemoveLegacyLocalEntries(std::vector<ModelInfo>& model_infos) {
std::erase_if(model_infos, [](const auto& info) {
const auto* provider = info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_MODEL_PROVIDER_STR);
Expand Down Expand Up @@ -194,6 +321,7 @@ std::vector<Model> AzureModelCatalog::FetchModels() const {
logger_.Log(LogLevel::Information, fmt::format("Found {} locally cached models.", cached_model_ids.size()));

auto catalog_result = GetLiveCatalogOrLocalSnapshot(cached_model_ids);
catalog_result.model_infos = FilterVisibleInfos(std::move(catalog_result.model_infos), ep_detector_, logger_);
auto models = CreateModelsWithLocalPaths(catalog_result.model_infos, local_models);

logger_.Log(LogLevel::Information, fmt::format("Populated model info for {} models.", models.size()));
Expand Down Expand Up @@ -221,6 +349,7 @@ std::vector<Model> AzureModelCatalog::FetchModelVersions(
try {
auto client = CreateCatalogClient(url, filter.value_or(""));
auto model_infos = client->FetchAllVersionsByAlias(model_alias, model_name);
model_infos = FilterVisibleInfos(std::move(model_infos), ep_detector_, logger_);

out.reserve(out.size() + model_infos.size());
for (auto& info : model_infos) {
Expand Down Expand Up @@ -265,6 +394,7 @@ std::vector<Model> AzureModelCatalog::FetchModelsByIds(const std::vector<std::st
try {
auto client = CreateCatalogClient(url, filter.value_or(""));
auto model_infos = client->FetchModelsByIds(remaining);
model_infos = FilterVisibleInfos(std::move(model_infos), ep_detector_, logger_);

for (auto& info : model_infos) {
std::string local_path;
Expand Down
53 changes: 32 additions & 21 deletions sdk_v2/cpp/src/catalog/local_model_scanner.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "catalog/local_model_scanner.h"
#include "util/model_layout.h"

#include <fmt/format.h>
#include <nlohmann/json.hpp>
Expand All @@ -15,7 +16,6 @@ namespace fs = std::filesystem;

namespace {

const char* kGenAIConfigFileName = "genai_config.json";
const char* kDownloadSignalFileName = "download.tmp";
const char* kInferenceModelFileName = "inference_model.json";

Expand Down Expand Up @@ -44,22 +44,34 @@ std::string ReadModelNameFromInferenceModel(const fs::path& dir) {
return {};
}

/// Check if a directory is a valid cached model directory.
/// Must have genai_config.json, no download.tmp, and an inference_model.json with a Name.
bool IsValidModelDirectory(const fs::path& dir) {
if (!fs::exists(dir / kGenAIConfigFileName)) {
bool HasCompletionMarker(const fs::path& dir) {
std::error_code ec;
const auto is_regular_file = fs::is_regular_file(dir / kInferenceModelFileName, ec);
return !ec && is_regular_file;
}

bool IsCompleteModelDirectory(const fs::path& dir) {
std::error_code ec;
const auto download_in_progress = fs::exists(dir / kDownloadSignalFileName, ec);
if (ec || download_in_progress) {
return false;
}

if (fs::exists(dir / kDownloadSignalFileName)) {
return false; // Incomplete download.
return HasCompletionMarker(dir);
}

void AddModelDirectory(const fs::path& dir, std::map<std::string, std::string>& results) {
auto model_name = ReadModelNameFromInferenceModel(dir);
if (model_name.empty()) {
return;
}

if (!fs::exists(dir / kInferenceModelFileName)) {
return false;
// If the model name doesn't contain a ':' version separator, append ":0".
if (model_name.find(':') == std::string::npos) {
model_name += ":0";
}

return true;
results[model_name] = dir.string();
}

/// Recursively scan a directory for valid model directories.
Expand All @@ -71,19 +83,18 @@ void ScanDirectory(const fs::path& dir,
return;
}

// Check if this directory itself is a valid model directory.
if (IsValidModelDirectory(dir)) {
auto model_name = ReadModelNameFromInferenceModel(dir);
if (!model_name.empty()) {
// If the model name doesn't contain a ':' version separator, append ":0".
if (model_name.find(':') == std::string::npos) {
model_name += ":0";
}

results[model_name] = dir.string();
const auto layout = ClassifyModelLayout(dir);
if (layout == ModelLayout::ModelPackage) {
if (IsCompleteModelDirectory(dir)) {
AddModelDirectory(dir, results);
}

// Don't recurse into a valid model directory — it's a leaf.
// Package children are implementation details, not independently loadable models.
return;
}

if (layout == ModelLayout::FlatModel && IsCompleteModelDirectory(dir)) {
AddModelDirectory(dir, results);
return;
}

Expand Down
Loading
Loading