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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions families/timm_mobilenetv3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# timm MobileNetV3

This family implements `image_to_class_scores` through `IImageToClassScores`.
The family binds the interface on its loaded model; the shared C ABI and the
header-only C++ `ImageToClassScores` wrapper discover that binding without a
family-specific shared registry.

The input is contiguous host RGB float32 in `[0, 1]`. The family owns the
checkpoint's resize, crop, normalization and engine call. The output contains
every class logit in checkpoint order, without softmax or top-k truncation.
`label_names` and `vocabulary_id` are retained when explicitly supplied by the
checkpoint. Missing metadata is returned as empty labels/identity: the class
indices remain model-local ordinals. There are no runtime Config options;
unsupported overrides are rejected.

Build a new bundle with this family version; the old `classification` bundle
mode is not retained. The existing CLI command remains simple:

```sh
trtmc build timm/mobilenetv3_large_100.ra_in1k -o mobilenetv3.bundle
trtmc classify mobilenetv3.bundle --image photo.jpg
```

## Validation

The existing official-checkpoint test still compares the native top-1 class
against the timm reference using the original image and preprocessing. It also
checks the complete logits and class metadata exposed by the semantic Task.
That same E2E executes both public SDK consumers, using the existing
`TRTMC_NATIVE_BUILD_DIR` to locate their built binaries. They receive identical
decoded RGB input, must agree on every score, and retain the original top-1
reference check. No new CI selector or environment variable is introduced.

The CPU contract test checks binding, unknown and explicit class identity,
normalization, complete owned output, and rejection before inference of invalid
input or unsupported Config. It also builds the two public SDK consumers:

```sh
cmake --build build --target test_timm_mobilenetv3_task_contract test_timm_mobilenetv3_image_preprocess
ctest --test-dir build --output-on-failure -R '^timm_mobilenetv3_(task_contract|image_preprocess)$'
```

`tests/sdk_consumer.c` and `tests/sdk_consumer.cpp` use only the public SDK. For
an already-built bundle, supply an unprocessed RGB float32 HWC file and its
original height and width:

```sh
build/test_timm_mobilenetv3_sdk_c mobilenetv3.bundle build image.rgb.f32 480 640
build/test_timm_mobilenetv3_sdk_cpp mobilenetv3.bundle build image.rgb.f32 480 640
```

Each prints all scores as JSON and reads the result after releasing its model
handle. These commands perform real inference and require the matching runtime
and checkpoint bundle; the CPU contract test alone does not qualify a checkpoint.

## Benchmark timing

`tests/performance.yaml` takes over the existing `timm_mobilenetv3.classify`
entry through the benchmark's family-owned reference protocol. The workload,
precision, 3 warmups, 10 measurements, 5% margin and top-class oracle are unchanged.
The reference times inference, complete float32 host logits and synchronization;
argmax, finite checks and JSON reporting happen after timing, as in the semantic
SDK benchmark worker. The existing reference policy still excludes input
preparation (`task-model-call-wall`), while the native public Task call includes
family preprocessing. This fixes reduction/reporting placement, not that existing
scope difference, and does not establish a performance improvement.
23 changes: 21 additions & 2 deletions families/timm_mobilenetv3/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,10 +459,26 @@ def get_bundle_config_overrides(self, config: ModelConfig) -> dict:
cfg = config.raw.get("_timm_mobilenetv3_config")
if cfg is None:
raise RuntimeError("load_weights must run before reading bundle config")
# Only publish checkpoint-supplied identity; absent metadata means
# model-local class ordinals, not an inferred cross-model vocabulary.
vocabulary_id = config.raw.get("vocabulary_id", "")
labels = config.raw.get("label_names", [])
if not isinstance(vocabulary_id, str):
raise ValueError("timm MobileNetV3 vocabulary_id must be a string")
if not isinstance(labels, list) or (
labels
and (
len(labels) != cfg["num_classes"]
or any(not isinstance(label, str) or not label for label in labels)
)
):
raise ValueError("timm MobileNetV3 label_names must name every class in order")
return {
"input_image_h": cfg["image_size_h"],
"input_image_w": cfg["image_size_w"],
"num_classes": cfg["num_classes"],
"vocabulary_id": vocabulary_id,
"labels": labels,
"image_mean": cfg["mean"],
"image_std": cfg["std"],
"crop_pct": cfg["crop_pct"],
Expand All @@ -487,8 +503,8 @@ def build(request: "BuildRequest", writer: "BundleWriter") -> None:
raise NotImplementedError("timm_mobilenetv3 does not support tensor parallelism")
if request.context_parallel_size != 1:
raise NotImplementedError("timm_mobilenetv3 does not support context parallelism")
if request.task != "classification":
raise ValueError("timm_mobilenetv3 supports only task=classification")
if request.task != "image_to_class_scores":
raise ValueError("timm_mobilenetv3 supports only task=image_to_class_scores")
if request.quantization not in {None, "none"}:
raise NotImplementedError("timm_mobilenetv3 does not support quantization")
if request.fp32_layers:
Expand Down Expand Up @@ -523,6 +539,9 @@ def build(request: "BuildRequest", writer: "BundleWriter") -> None:
"interpolation",
"image_mean",
"image_std",
"num_classes",
"vocabulary_id",
"labels",
)
},
)
35 changes: 35 additions & 0 deletions families/timm_mobilenetv3/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ install(TARGETS trtmc_model_timm_mobilenetv3
)

if(TRTMC_BUILD_TESTS)
add_executable(test_timm_mobilenetv3_task_contract
${PROJECT_SOURCE_DIR}/families/timm_mobilenetv3/tests/cpp/test_task_contract.cpp
)
target_include_directories(test_timm_mobilenetv3_task_contract PRIVATE
${PROJECT_SOURCE_DIR}
${PROJECT_SOURCE_DIR}/core/runtime/include
)
target_include_directories(test_timm_mobilenetv3_task_contract SYSTEM PRIVATE
${TRTMC_CUDA_INCLUDE_DIR}
)
target_link_libraries(test_timm_mobilenetv3_task_contract PRIVATE
trtmc_model_timm_mobilenetv3 trtmc_core ${TRTMC_CUDART_LIBRARY}
)
target_compile_options(test_timm_mobilenetv3_task_contract PRIVATE
-Wall -Wextra -Wpedantic -Werror
)
add_test(NAME timm_mobilenetv3_task_contract COMMAND test_timm_mobilenetv3_task_contract)
set_tests_properties(timm_mobilenetv3_task_contract PROPERTIES LABELS "cpu")

foreach(_language IN ITEMS c cpp)
add_executable(test_timm_mobilenetv3_sdk_${_language}
${PROJECT_SOURCE_DIR}/families/timm_mobilenetv3/tests/sdk_consumer.${_language}
)
target_link_libraries(test_timm_mobilenetv3_sdk_${_language} PRIVATE trtmc_c)
target_compile_options(test_timm_mobilenetv3_sdk_${_language} PRIVATE
-Wall -Wextra -Wpedantic -Werror
)
set_target_properties(test_timm_mobilenetv3_sdk_${_language} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
endforeach()
add_dependencies(test_timm_mobilenetv3_task_contract
test_timm_mobilenetv3_sdk_c test_timm_mobilenetv3_sdk_cpp
)

add_executable(test_timm_mobilenetv3_image_preprocess
${PROJECT_SOURCE_DIR}/families/timm_mobilenetv3/tests/cpp/test_image_preprocess_seam.cpp
)
Expand Down
54 changes: 37 additions & 17 deletions families/timm_mobilenetv3/runtime/pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

#include "families/timm_mobilenetv3/runtime/pipeline.h"

#include <algorithm>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <utility>

Expand All @@ -18,44 +18,64 @@ const Tensor& require_logits(const TensorMap& outputs) {
const auto found = outputs.find("logits");
if (found == outputs.end())
throw std::runtime_error("timm MobileNetV3 engine did not return logits");
if (found->second.numel() <= 0)
throw std::runtime_error("timm MobileNetV3 engine returned empty logits");
if (found->second.data == nullptr || found->second.dtype != DType::kFloat32 ||
found->second.numel() <= 0)
throw std::runtime_error("timm MobileNetV3 engine must return nonempty float32 logits");
return found->second;
}

} // namespace

TimmMobilenetv3ImageClassificationPipeline::TimmMobilenetv3ImageClassificationPipeline(
std::unique_ptr<ITrtModule> model, TimmMobilenetv3PreprocessConfig preprocess_config)
: model_(std::move(model)), preprocess_config_(std::move(preprocess_config)) {
std::unique_ptr<ITrtModule> model, TimmMobilenetv3PreprocessConfig preprocess_config,
std::int32_t num_classes, std::string vocabulary_id, std::vector<std::string> labels)
: model_(std::move(model)), preprocess_config_(std::move(preprocess_config)),
num_classes_(num_classes), vocabulary_id_(std::move(vocabulary_id)),
labels_(std::move(labels)) {
if (!model_ || !model_->ok())
throw std::runtime_error("TimmMobilenetv3ImageClassificationPipeline: invalid model");
if (num_classes_ <= 0 ||
(!labels_.empty() && labels_.size() != static_cast<std::size_t>(num_classes_)))
throw std::runtime_error("timm MobileNetV3 class metadata does not match its output size");
}

ClassificationResult TimmMobilenetv3ImageClassificationPipeline::classify(const float* pixels,
int32_t height,
int32_t width) {
auto pixel_values =
preprocess_timm_mobilenetv3_image(pixels, height, width, preprocess_config_);
internal::LabelScoresResult
TimmMobilenetv3ImageClassificationPipeline::run(const internal::ImageToClassScoresRequest& request,
internal::ConfigView config) {
if (!config.empty())
throw internal::ConfigError("timm MobileNetV3 has no runtime configuration");
const auto& image = request.image;
if (image.format != internal::ImageFormat::Float32 || image.channels != 3 ||
image.data == nullptr || image.height == 0 || image.width == 0 ||
image.height > static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max()) ||
image.width > static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max()) ||
static_cast<std::uint64_t>(image.height) >
std::numeric_limits<std::size_t>::max() / image.width / 3 / sizeof(float) ||
image.byte_size != static_cast<std::size_t>(image.height) * image.width * 3 * sizeof(float))
throw std::invalid_argument("timm MobileNetV3 requires contiguous float32 RGB input");
auto pixel_values = preprocess_timm_mobilenetv3_image(
static_cast<const float*>(image.data), static_cast<std::int32_t>(image.height),
static_cast<std::int32_t>(image.width), preprocess_config_);

Tensor img_t;
img_t.data = pixel_values.data();
img_t.shape = {1, 3, preprocess_config_.input_image_h, preprocess_config_.input_image_w};
img_t.dtype = DType::kFloat32;

auto outputs = model_->forward({{"pixel_values", img_t}});
ClassificationResult result;
internal::LabelScoresResult result;

const auto& logits_tensor = require_logits(outputs);
const auto n = logits_tensor.numel();
if (n != static_cast<std::size_t>(num_classes_))
throw std::runtime_error("timm MobileNetV3 logits do not match its configured class count");

result.logits.resize(static_cast<std::size_t>(n));
std::memcpy(result.logits.data(), logits_tensor.data,
result.scores.resize(static_cast<std::size_t>(n));
std::memcpy(result.scores.data(), logits_tensor.data,
static_cast<std::size_t>(n) * sizeof(float));

auto best = std::max_element(result.logits.begin(), result.logits.end());
result.top_class = static_cast<int32_t>(std::distance(result.logits.begin(), best));
result.top_score = (best == result.logits.end()) ? 0.0F : *best;
result.kind = internal::ScoreKind::Logit;
result.vocabulary_id = vocabulary_id_;
result.labels = labels_;
return result;
}

Expand Down
19 changes: 15 additions & 4 deletions families/timm_mobilenetv3/runtime/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,34 @@
#pragma once

#include "families/timm_mobilenetv3/runtime/image_preprocess_seam.h"
#include "trtmc/internal/features.h"
#include "trtmc/internal/model.h"
#include "trtmc/runtime/trt_module.h"
#include "trtmc/task.h"

#include <memory>

namespace trtmc {

class TimmMobilenetv3ImageClassificationPipeline final : public IImageClassification {
class TimmMobilenetv3ImageClassificationPipeline final : public internal::IModel,
public internal::IImageToClassScores {
public:
explicit TimmMobilenetv3ImageClassificationPipeline(
std::unique_ptr<ITrtModule> model, TimmMobilenetv3PreprocessConfig preprocess_config);
std::unique_ptr<ITrtModule> model, TimmMobilenetv3PreprocessConfig preprocess_config,
std::int32_t num_classes, std::string vocabulary_id, std::vector<std::string> labels);

ClassificationResult classify(const float* pixels, int32_t height, int32_t width) override;
const char* task() const noexcept override { return IImageToClassScores::kTask.data(); }
std::vector<internal::TaskInstance> task_bindings() override {
return {internal::bind<internal::IImageToClassScores>(*this)};
}
internal::LabelScoresResult run(const internal::ImageToClassScoresRequest& request,
internal::ConfigView config) override;

private:
std::unique_ptr<ITrtModule> model_;
TimmMobilenetv3PreprocessConfig preprocess_config_;
std::int32_t num_classes_;
std::string vocabulary_id_;
std::vector<std::string> labels_;
};

} // namespace trtmc
12 changes: 7 additions & 5 deletions families/timm_mobilenetv3/runtime/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ std::vector<char> require_section(const BundleReader& bundle, const char* name)
return bundle.read_section(name);
}

TimmMobilenetv3PreprocessConfig parse_config(const std::vector<char>& data) {
const auto json = nlohmann::json::parse(data.begin(), data.end());
TimmMobilenetv3PreprocessConfig parse_config(const nlohmann::json& json) {
TimmMobilenetv3PreprocessConfig config;
config.input_image_h = json.at("input_image_h").get<std::int32_t>();
config.input_image_w = json.at("input_image_w").get<std::int32_t>();
Expand Down Expand Up @@ -57,8 +56,11 @@ extern "C" trtmc::ITask* trtmc_create_family(const trtmc::FamilyContext& context
const auto& config_data =
trtmc::timm_mobilenetv3::require_section(context.reader, "runtime.json");
const auto& plan = trtmc::timm_mobilenetv3::require_section(context.reader, "engine.plan");
auto config = trtmc::timm_mobilenetv3::parse_config(config_data);
const auto metadata = nlohmann::json::parse(config_data.begin(), config_data.end());
auto config = trtmc::timm_mobilenetv3::parse_config(metadata);
auto engine = trtmc::timm_mobilenetv3::load_engine(context.backend, plan);
return new trtmc::TimmMobilenetv3ImageClassificationPipeline(std::move(engine),
std::move(config));
return new trtmc::TimmMobilenetv3ImageClassificationPipeline(
std::move(engine), std::move(config), metadata.at("num_classes").get<std::int32_t>(),
metadata.at("vocabulary_id").get<std::string>(),
metadata.at("labels").get<std::vector<std::string>>());
}
4 changes: 2 additions & 2 deletions families/timm_mobilenetv3/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@

describe = family_support(
architectures=("mobilenetv3_large_100",),
tasks=("classification",),
default_task="classification",
tasks=("image_to_class_scores",),
default_task="image_to_class_scores",
)
Loading
Loading