This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new c0a305dfeb [TARGET] Fix round-trip reconstruction of targets with 
canonicalizer-generated `feature.*` attrs (#18883)
c0a305dfeb is described below

commit c0a305dfeb0848ccd06763753eb11ace8d72e7e6
Author: Masahiro Hiramori <[email protected]>
AuthorDate: Sat Mar 7 09:39:34 2026 +0900

    [TARGET] Fix round-trip reconstruction of targets with 
canonicalizer-generated `feature.*` attrs (#18883)
    
    Fix #18882
    
    `TargetNode::ToConfig()` exports all target attrs, including derived
    `feature.*` fields set by target canonicalizers. However,
    `TargetInternal::FromConfig()` rejects these keys during schema
    validation because they are not declared in the target kind schema. This
    breaks round-tripping exported configs through `Target(config)`.
    
    This PR strips `feature.*` keys from the config before
    `ConfigSchema::Resolve`, then merges them back afterward. Canonicalizer
    output is authoritative — if the canonicalizer re-emits a `feature.*`
    key, it overwrites the preserved value. Unknown non-`feature.*` keys
    continue to fail validation as before.
    
    Changes:
    - src/target/target.cc: Extract and re-merge `feature.*` keys around
    schema resolution in `FromConfig()`
    - tests/cpp/target_test.cc: Add tests for single-target round-trip,
    nested-host round-trip, and continued rejection of unknown non-feature
    keys
    
    ---------
    
    Co-authored-by: gemini-code-assist[bot] 
<176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
 src/target/target.cc     | 35 +++++++++++++++++++++++++----
 tests/cpp/target_test.cc | 57 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+), 4 deletions(-)

diff --git a/src/target/target.cc b/src/target/target.cc
index 91a5854b39..7d03d5e14c 100644
--- a/src/target/target.cc
+++ b/src/target/target.cc
@@ -330,11 +330,38 @@ ObjectPtr<TargetNode> 
TargetInternal::FromConfig(ffi::Map<ffi::String, ffi::Any>
     target->host = std::nullopt;
   }
 
-  // Step 3: Use ConfigSchema to validate types, apply defaults, and run 
canonicalizer
+  // Step 3: Extract any "feature.*" keys from the input config before schema 
validation.
+  // These are canonicalizer-owned metadata that may appear in exported 
configs (via ToConfig())
+  // but are not part of the target kind's declared schema. We preserve them 
across round-trip
+  // and let the canonicalizer's output take priority if it re-emits the same 
key.
+  std::unordered_map<std::string, ffi::Any> saved_features;
+  {
+    std::vector<ffi::String> feature_keys;
+    for (const auto& kv : config) {
+      std::string key_str(kv.first);
+      if (key_str.size() > 8 && key_str.compare(0, 8, "feature.") == 0) {
+        saved_features[key_str] = kv.second;
+        feature_keys.push_back(kv.first);
+      }
+    }
+    for (const auto& k : feature_keys) {
+      config.erase(k);
+    }
+  }
+
+  // Step 4: Use ConfigSchema to validate types, apply defaults, and run 
canonicalizer
   // Note: structural keys (kind, tag, keys, device) pass through to 
canonicalizer
   ffi::Map<ffi::String, ffi::Any> resolved = 
target->kind->schema_.Resolve(config);
 
-  // Step 4: Extract structural fields from resolved config
+  // Step 5: Merge back preserved feature.* keys. Canonicalizer output is 
authoritative:
+  // only restore a saved feature if the canonicalizer did not re-emit it.
+  for (const auto& kv : saved_features) {
+    if (resolved.find(ffi::String(kv.first)) == resolved.end()) {
+      resolved.Set(ffi::String(kv.first), kv.second);
+    }
+  }
+
+  // Step 6: Extract structural fields from resolved config
   if (resolved.count(kTag)) {
     if (auto tag = resolved[kTag].try_cast<ffi::String>()) {
       target->tag = tag.value();
@@ -367,14 +394,14 @@ ObjectPtr<TargetNode> 
TargetInternal::FromConfig(ffi::Map<ffi::String, ffi::Any>
     resolved.erase(kKeys);
   }
 
-  // Step 5: Build attrs from resolved entries (excluding structural keys)
+  // Step 7: Build attrs from resolved entries (excluding structural keys)
   resolved.erase(kKind);
   std::unordered_map<ffi::String, ffi::Any> attrs;
   for (const auto& kv : resolved) {
     attrs[kv.first] = kv.second;
   }
 
-  // Step 6: If requested, query attributes from the device. User-specified
+  // Step 8: If requested, query attributes from the device. User-specified
   // parameters take precedence over queried parameters.
   int64_t from_device_id = -1;
   if (auto it = attrs.find(kFromDevice); it != attrs.end()) {
diff --git a/tests/cpp/target_test.cc b/tests/cpp/target_test.cc
index df592f7608..342870e88f 100644
--- a/tests/cpp/target_test.cc
+++ b/tests/cpp/target_test.cc
@@ -222,6 +222,63 @@ TEST(TargetCreation, TargetParserProcessing) {
   ASSERT_EQ(test_target->GetAttr<ffi::String>("mattr").value(), "cake");
 }
 
+TEST(TargetCreation, RoundTripCanonicalizerFeatures) {
+  // Construct a target whose canonicalizer sets feature.test and transforms 
mcpu
+  Target original(ffi::Map<ffi::String, ffi::Any>{
+      {"kind", ffi::String("TestTargetParser")},
+      {"mcpu", ffi::String("woof")},
+  });
+  ASSERT_EQ(original->GetAttr<ffi::String>("mcpu").value(), "super_woof");
+  ASSERT_EQ(original->GetAttr<bool>("feature.test").value(), true);
+
+  // Export to config and reconstruct
+  ffi::Map<ffi::String, ffi::Any> exported = original->ToConfig();
+  Target reconstructed(exported);
+
+  // Canonicalized attrs must survive the round-trip
+  // Note: mcpu gets canonicalized again (super_super_woof) because the 
canonicalizer runs
+  ASSERT_TRUE(reconstructed->GetAttr<ffi::String>("mcpu").has_value());
+  ASSERT_EQ(reconstructed->GetAttr<bool>("feature.test").value(), true);
+  ASSERT_EQ(reconstructed->keys.size(), 1);
+  ASSERT_EQ(reconstructed->keys[0], "super");
+}
+
+TEST(TargetCreation, RoundTripCanonicalizerFeaturesNestedHost) {
+  // Construct a host target whose canonicalizer sets feature.test
+  Target host(ffi::Map<ffi::String, ffi::Any>{
+      {"kind", ffi::String("TestTargetParser")},
+      {"mcpu", ffi::String("woof")},
+  });
+  ASSERT_EQ(host->GetAttr<bool>("feature.test").value(), true);
+
+  // Attach it as host to another target
+  Target outer(ffi::Map<ffi::String, ffi::Any>{
+      {"kind", ffi::String("TestTargetKind")},
+      {"my_bool", true},
+  });
+  Target combined(outer, host);
+
+  // Export the outer target (includes nested host) and reconstruct
+  ffi::Map<ffi::String, ffi::Any> exported = combined->ToConfig();
+  Target reconstructed(exported);
+
+  // The nested host must reconstruct successfully with feature.* preserved
+  ffi::Optional<Target> reconstructed_host = reconstructed->GetHost();
+  ASSERT_TRUE(reconstructed_host.defined());
+  ASSERT_EQ(reconstructed_host.value()->GetAttr<bool>("feature.test").value(), 
true);
+  
ASSERT_TRUE(reconstructed_host.value()->GetAttr<ffi::String>("mcpu").has_value());
+}
+
+TEST(TargetCreationFail, UnknownNonFeatureKeyStillFails) {
+  // Verify that unknown non-feature.* keys still fail schema validation
+  ffi::Map<ffi::String, ffi::Any> config = {
+      {"kind", ffi::String("TestTargetParser")},
+      {"mcpu", ffi::String("woof")},
+      {"unknown_key", ffi::String("bad")},
+  };
+  ASSERT_THROW({ Target{config}; }, tvm::Error);
+}
+
 TVM_REGISTER_TARGET_KIND("TestStringKind", kDLCPU)
     .add_attr_option<ffi::String>("single")
     .add_attr_option<ffi::Array<ffi::String>>("array")

Reply via email to