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
66 changes: 66 additions & 0 deletions families/timm_dpn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# timm DPN

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/dpn68b.ra_in1k -o model.bundle
trtmc classify model.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_dpn_task_contract test_timm_dpn_image_preprocess
ctest --test-dir build --output-on-failure -R '^timm_dpn_(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_dpn_sdk_c model.bundle build image.rgb.f32 480 640
build/test_timm_dpn_sdk_cpp model.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_dpn.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.
16 changes: 14 additions & 2 deletions families/timm_dpn/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,8 @@ def build(request: "BuildRequest", writer: "BundleWriter") -> None:
raise NotImplementedError("timm_dpn does not support tensor parallelism")
if request.context_parallel_size != 1:
raise NotImplementedError("timm_dpn does not support context parallelism")
if request.task != "classification":
raise ValueError("timm_dpn supports only task=classification")
if request.task != "image_to_class_scores":
raise ValueError("timm_dpn supports only task=image_to_class_scores")
if request.quantization not in {None, "none"}:
raise NotImplementedError("timm_dpn does not support quantization")
if request.fp32_layers:
Expand All @@ -468,6 +468,15 @@ def build(request: "BuildRequest", writer: "BundleWriter") -> None:
raise NotImplementedError("timm_dpn supports only max_sequence_length=1")
model_dir = Path(request.model_dir)
raw = _read_config(model_dir)
vocabulary_id = raw.get("vocabulary_id", "")
labels = raw.get("label_names", [])
if not isinstance(vocabulary_id, str):
raise ValueError("timm DPN vocabulary_id must be a string")
if not isinstance(labels, list) or (labels and (
len(labels) != _preprocess_config(raw)["num_classes"]
or any(not isinstance(label, str) or not label for label in labels)
)):
raise ValueError("timm DPN label_names must name every class")
plan, runtime = _build_engine(
raw,
Checkpoint.open(model_dir),
Expand All @@ -479,6 +488,9 @@ def build(request: "BuildRequest", writer: "BundleWriter") -> None:
writer.add_json(
"runtime.json",
{
"num_classes": runtime["num_classes"],
"vocabulary_id": vocabulary_id,
"labels": labels,
"input_image_h": runtime["image_height"],
"input_image_w": runtime["image_width"],
"crop_pct": runtime["crop_pct"],
Expand Down
35 changes: 35 additions & 0 deletions families/timm_dpn/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,41 @@ install(TARGETS trtmc_model_timm_dpn
)

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

foreach(_language IN ITEMS c cpp)
add_executable(test_timm_dpn_sdk_${_language}
${PROJECT_SOURCE_DIR}/families/timm_dpn/tests/sdk_consumer.${_language}
)
target_link_libraries(test_timm_dpn_sdk_${_language} PRIVATE trtmc_c)
target_compile_options(test_timm_dpn_sdk_${_language} PRIVATE
-Wall -Wextra -Wpedantic -Werror
)
set_target_properties(test_timm_dpn_sdk_${_language} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
endforeach()
add_dependencies(test_timm_dpn_task_contract
test_timm_dpn_sdk_c test_timm_dpn_sdk_cpp
)

add_executable(test_timm_dpn_image_preprocess
${PROJECT_SOURCE_DIR}/families/timm_dpn/tests/cpp/test_image_preprocess_seam.cpp
)
Expand Down
84 changes: 59 additions & 25 deletions families/timm_dpn/runtime/pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,82 @@

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

namespace trtmc {

namespace {

const Tensor* find_logits(const TensorMap& outputs) {
const Tensor& require_logits(const TensorMap& outputs) {
for (const auto& [name, tensor] : outputs) {
if (name.find("logits") != std::string::npos || outputs.size() == 1)
return &tensor;
if (name.find("logits") == std::string::npos && outputs.size() != 1)
continue;
if (tensor.data == nullptr || tensor.dtype != DType::kFloat32 || tensor.numel() <= 0)
throw std::runtime_error("timm DPN engine must return nonempty float32 logits");
return tensor;
}
return nullptr;
throw std::runtime_error("timm DPN engine did not return logits");
}

} // namespace

TimmDpnImageClassificationPipeline::TimmDpnImageClassificationPipeline(
std::unique_ptr<ITrtModule> model, TimmDpnPreprocessConfig preprocess_config)
: model_(std::move(model)), preprocess_config_(std::move(preprocess_config)) {
std::unique_ptr<ITrtModule> model, TimmDpnPreprocessConfig 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("TimmDpnImageClassificationPipeline: invalid model");
if (num_classes_ <= 0 ||
(!labels_.empty() && labels_.size() != static_cast<std::size_t>(num_classes_)))
throw std::runtime_error("timm DPN class metadata does not match its output size");
Comment thread
yifeif-nv marked this conversation as resolved.
if (vocabulary_id_.empty() &&
std::any_of(labels_.begin(), labels_.end(),
[](const std::string& label) { return label.empty(); }))
throw std::runtime_error(
"class labels require nonempty names without an explicit vocabulary identity");
}

ClassificationResult TimmDpnImageClassificationPipeline::classify(const float* pixels,
int32_t height, int32_t width) {
auto values = preprocess_timm_dpn_image(pixels, height, width, preprocess_config_);
Tensor input;
input.data = values.data();
input.shape = {1, 3, preprocess_config_.input_image_h, preprocess_config_.input_image_w};
input.dtype = DType::kFloat32;
const auto outputs = model_->forward({{"pixel_values", input}});
const Tensor* logits = find_logits(outputs);
if (logits == nullptr || logits->numel() <= 0)
throw std::runtime_error("timm DPN engine returned no logits");
if (logits->dtype != DType::kFloat32)
throw std::runtime_error("timm DPN logits must be float32");
ClassificationResult result;
result.logits.resize(static_cast<std::size_t>(logits->numel()));
std::memcpy(result.logits.data(), logits->data, result.logits.size() * sizeof(float));
const 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;
internal::LabelScoresResult
TimmDpnImageClassificationPipeline::run(const internal::ImageToClassScoresRequest& request,
internal::ConfigView config) {
if (!config.empty())
throw internal::ConfigError("timm DPN 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 DPN requires contiguous float32 RGB input");
auto pixel_values = preprocess_timm_dpn_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}});
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 DPN logits do not match its configured class count");

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));
result.kind = internal::ScoreKind::Logit;
result.vocabulary_id = vocabulary_id_;
result.labels = labels_;
return result;
}

Expand Down
20 changes: 16 additions & 4 deletions families/timm_dpn/runtime/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,35 @@
#pragma once

#include "families/timm_dpn/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 TimmDpnImageClassificationPipeline final : public IImageClassification {
class TimmDpnImageClassificationPipeline final : public internal::IModel,
public internal::IImageToClassScores {
public:
explicit TimmDpnImageClassificationPipeline(std::unique_ptr<ITrtModule> model,
TimmDpnPreprocessConfig preprocess_config = {});
TimmDpnPreprocessConfig 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_;
TimmDpnPreprocessConfig preprocess_config_;
std::int32_t num_classes_;
std::string vocabulary_id_;
std::vector<std::string> labels_;
};

} // namespace trtmc
17 changes: 10 additions & 7 deletions families/timm_dpn/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);
}

TimmDpnPreprocessConfig parse_config(const std::vector<char>& data) {
const auto json = nlohmann::json::parse(data.begin(), data.end());
TimmDpnPreprocessConfig parse_config(const nlohmann::json& json) {
TimmDpnPreprocessConfig 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 All @@ -34,7 +33,7 @@ TimmDpnPreprocessConfig parse_config(const std::vector<char>& data) {
if (config.input_image_h <= 0 || config.input_image_w <= 0 || config.crop_pct <= 0.0F ||
config.crop_pct > 1.0F || config.image_mean.size() != 3 || config.image_std.size() != 3 ||
(config.interpolation != "bilinear" && config.interpolation != "bicubic")) {
throw std::runtime_error("timm DPN runtime.json does not match its contract");
throw std::runtime_error("timm DPN runtime.json does not match its runtime contract");
}
return config;
}
Expand All @@ -53,9 +52,13 @@ std::unique_ptr<ITrtModule> load_engine(IBackend& backend, const std::vector<cha
extern "C" trtmc::ITask* trtmc_create_family(const trtmc::FamilyContext& context) {
if (context.kv_cache_size_bytes != 0)
throw std::invalid_argument("timm_dpn does not support --kv-cache-size");
const auto config_data = trtmc::timm_dpn::require_section(context.reader, "runtime.json");
const auto plan = trtmc::timm_dpn::require_section(context.reader, "engine.plan");
auto config = trtmc::timm_dpn::parse_config(config_data);
const auto& config_data = trtmc::timm_dpn::require_section(context.reader, "runtime.json");
const auto& plan = trtmc::timm_dpn::require_section(context.reader, "engine.plan");
const auto metadata = nlohmann::json::parse(config_data.begin(), config_data.end());
auto config = trtmc::timm_dpn::parse_config(metadata);
auto engine = trtmc::timm_dpn::load_engine(context.backend, plan);
return new trtmc::TimmDpnImageClassificationPipeline(std::move(engine), std::move(config));
return new trtmc::TimmDpnImageClassificationPipeline(
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_dpn/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
describe = family_support(
model_types=("timm_dpn", "dpn68", "dpn68b", "dpn92", "dpn98", "dpn107", "dpn131"),
architectures=("dpn68", "dpn68b", "dpn92", "dpn98", "dpn107", "dpn131"),
tasks=("classification",),
default_task="classification",
tasks=("image_to_class_scores",),
default_task="image_to_class_scores",
)
Loading
Loading