diff --git a/projects/hipdnn/python/frontend_bindings/src/graph_bindings.cpp b/projects/hipdnn/python/frontend_bindings/src/graph_bindings.cpp index 33a64bd27a81..2da76b074535 100644 --- a/projects/hipdnn/python/frontend_bindings/src/graph_bindings.cpp +++ b/projects/hipdnn/python/frontend_bindings/src/graph_bindings.cpp @@ -160,6 +160,12 @@ std::pair, AutotuneCacheWriteOutcome> return {results, outcome}; } +template +std::shared_ptr tensorScalar(T value, graph::ScalarType scalarType) +{ + return graph::Graph::tensor(value, scalarType); +} + } // namespace void graphBindings(nb::module_& m) @@ -802,6 +808,24 @@ void graphBindings(nb::module_& m) "Create a tensor with the metadata of a hipdnn Tensor or a __dlpack__ producer. " "A host (cpu) producer becomes a runtime pass-by-value tensor " "whose value is passed to execute() as a host tensor.") + .def_static("tensor_scalar", + &tensorScalar, + nb::arg("value"), + nb::arg("scalar_type"), + "Create a pass-by-value scalar tensor. scalar_type selects a compile-time " + "constant or a runtime parameter with a default value.") + .def_static("tensor_scalar", + &tensorScalar, + nb::arg("value"), + nb::arg("scalar_type")) + .def_static("tensor_scalar", + &tensorScalar, + nb::arg("value"), + nb::arg("scalar_type")) + .def_static("tensor_scalar", + &tensorScalar, + nb::arg("value"), + nb::arg("scalar_type")) .def( "to_json", [](graph::Graph& g) { diff --git a/projects/hipdnn/python/frontend_bindings/src/tensor_bindings.cpp b/projects/hipdnn/python/frontend_bindings/src/tensor_bindings.cpp index 4467065d18b7..8bcf8552afbb 100644 --- a/projects/hipdnn/python/frontend_bindings/src/tensor_bindings.cpp +++ b/projects/hipdnn/python/frontend_bindings/src/tensor_bindings.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include namespace nb = nanobind; using namespace hipdnn_frontend; @@ -110,10 +112,39 @@ TensorAttributes& } } +// Converts a stored scalar to a plain Python value; an empty value becomes None. +nb::object valueToPython(const TensorAttributes::ValueVariant& value) +{ + return std::visit( + [](const auto& v) -> nb::object { + using T = std::decay_t; + if constexpr(std::is_same_v) + { + return nb::none(); + } + else if constexpr(std::is_same_v) + { + return nb::bool_(v); + } + else if constexpr(std::is_integral_v) + { + return nb::int_(static_cast(v)); + } + else + { + return nb::float_(static_cast(v)); + } + }, + value); +} + } // namespace void tensorBindings(nb::module_& m) { + nb::enum_(m, "ScalarType") + .value("RUNTIME_PARAM", ScalarType::RUNTIME_PARAM) + .value("COMPILE_TIME_CONST", ScalarType::COMPILE_TIME_CONST); nb::class_(m, "Tensor") .def(nb::init<>()) .def_static( @@ -149,6 +180,12 @@ void tensorBindings(nb::module_& m) "data_type, else the tensor's current type, else inferred from value " "(bool -> BOOLEAN, int -> INT64, float -> FLOAT). Resets dims and strides to " "[1]; the tensor must have one element.") + .def("set_compile_time_constant", + &setValue, + nb::arg("value"), + nb::arg("data_type") = nb::none(), + nb::rv_policy::reference_internal, + "Alias of set_value(): store a compile-time constant scalar.") .def("get_is_pass_by_value", &TensorAttributes::get_is_pass_by_value) .def("get_is_runtime_pass_by_value", &TensorAttributes::get_is_runtime_pass_by_value) .def("set_is_pass_by_value", @@ -157,5 +194,37 @@ void tensorBindings(nb::module_& m) .def("get_volume", &TensorAttributes::get_volume) .def("has_uid", &TensorAttributes::has_uid) .def("clear_uid", &TensorAttributes::clear_uid, nb::rv_policy::reference_internal) + .def("get_alignment", &TensorAttributes::get_alignment) + .def("set_alignment", &TensorAttributes::set_alignment, nb::rv_policy::reference_internal) + .def("get_ragged_offset_multiplier", &TensorAttributes::get_ragged_offset_multiplier) + .def("set_ragged_offset_multiplier", + &TensorAttributes::set_ragged_offset_multiplier, + nb::rv_policy::reference_internal) + .def("has_ragged_offset_multiplier", &TensorAttributes::has_ragged_offset_multiplier) + .def("clear_value", &TensorAttributes::clear_value, nb::rv_policy::reference_internal) + .def("set_as_runtime_parameter", + &TensorAttributes::set_as_runtime_parameter, + nb::rv_policy::reference_internal) + .def("get_has_compile_time_constant", &TensorAttributes::get_has_compile_time_constant) + .def("has_ragged_offset", &TensorAttributes::has_ragged_offset) + .def("get_ragged_offset", &TensorAttributes::get_ragged_offset) + .def("set_ragged_offset", + &TensorAttributes::set_ragged_offset, + nb::arg("value").none(), + nb::rv_policy::reference_internal) + .def("get_value_variant", + [](const TensorAttributes& tensor) { + return valueToPython(tensor.get_value_variant()); + }) + .def("get_pass_by_value", + [](const TensorAttributes& tensor) { + return valueToPython( + tensor.get_pass_by_value().value_or(TensorAttributes::ValueVariant{})); + }) + .def("get_compile_time_constant", + [](const TensorAttributes& tensor) { + return valueToPython( + tensor.get_compile_time_constant().value_or(TensorAttributes::ValueVariant{})); + }) .def("validate", &TensorAttributes::validate); } diff --git a/projects/hipdnn/python/frontend_wheel_package/tests/test_tensor.py b/projects/hipdnn/python/frontend_wheel_package/tests/test_tensor.py index 7e5341196c69..0aaaa1f832fc 100644 --- a/projects/hipdnn/python/frontend_wheel_package/tests/test_tensor.py +++ b/projects/hipdnn/python/frontend_wheel_package/tests/test_tensor.py @@ -90,6 +90,126 @@ def test_is_virtual_round_trip(self): assert tensor.get_is_virtual() + def test_alignment_round_trip(self): + """set_alignment()/get_alignment() round-trip.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + + assert tensor.get_alignment() == 16 + tensor.set_alignment(32) + assert tensor.get_alignment() == 32 + + def test_set_alignment_returns_self(self): + """set_alignment() returns self for chaining.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + + assert tensor.set_alignment(32) is tensor + + def test_validate_rejects_alignment_below_one(self): + """validate() fails when alignment is less than 1.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + tensor.set_alignment(0) + + result = tensor.validate() + + assert not result.is_good() + assert " alignment must be >= 1" in result.get_message() + + def test_ragged_offset_multiplier_round_trip(self): + """Ragged offset multiplier setter/getter round-trip.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + + assert tensor.get_ragged_offset_multiplier() == 1 + assert not tensor.has_ragged_offset_multiplier() + + tensor.set_ragged_offset_multiplier(4) + + assert tensor.get_ragged_offset_multiplier() == 4 + assert tensor.has_ragged_offset_multiplier() + + def test_validate_rejects_multiplier_below_one(self): + """validate() fails when the ragged offset multiplier is less than 1.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + tensor.set_ragged_offset_multiplier(0) + + result = tensor.validate() + + assert not result.is_good() + assert " ragged offset multiplier must be >= 1" in result.get_message() + + def test_validate_rejects_multiplier_without_ragged_offset(self): + """validate() fails when a multiplier is set without a ragged offset.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + tensor.set_ragged_offset_multiplier(4) + + result = tensor.validate() + + assert not result.is_good() + assert ( + " ragged offset multiplier requires a ragged offset" in result.get_message() + ) + + def test_has_compile_time_constant_follows_set_value(self): + """set_value() makes a compile-time constant; clear_value() removes it.""" + tensor = hipdnn.Tensor.create([1], hipdnn.DataType.FLOAT) + + assert not tensor.get_has_compile_time_constant() + + tensor.set_value(2.5) + assert tensor.get_has_compile_time_constant() + + assert tensor.clear_value() is tensor + assert not tensor.get_has_compile_time_constant() + + def test_set_as_runtime_parameter(self): + """set_as_runtime_parameter() marks the tensor runtime pass-by-value.""" + tensor = hipdnn.Tensor.create([1], hipdnn.DataType.FLOAT) + tensor.set_value(2.5) + + assert tensor.set_as_runtime_parameter() is tensor + assert tensor.get_is_runtime_pass_by_value() + assert not tensor.get_has_compile_time_constant() + assert tensor.get_value_variant() is None + + def test_ragged_offset_round_trip(self): + """set_ragged_offset()/get_ragged_offset() round-trip.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + offset = hipdnn.Tensor.create([4], hipdnn.DataType.INT32) + + assert not tensor.has_ragged_offset() + assert tensor.get_ragged_offset() is None + + tensor.set_ragged_offset(offset) + + assert tensor.has_ragged_offset() + assert tensor.get_ragged_offset() is offset + + def test_set_ragged_offset_accepts_none(self): + """set_ragged_offset(None) clears the ragged offset.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + tensor.set_ragged_offset(hipdnn.Tensor.create([4], hipdnn.DataType.INT32)) + + tensor.set_ragged_offset(None) + + assert not tensor.has_ragged_offset() + assert tensor.get_ragged_offset() is None + + def test_multiplier_with_ragged_offset_validates(self): + """A multiplier is valid once a ragged offset is set.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + tensor.set_ragged_offset(hipdnn.Tensor.create([4], hipdnn.DataType.INT32)) + tensor.set_ragged_offset_multiplier(4) + + result = tensor.validate() + + assert result.is_good(), result.get_message() + + def test_set_ragged_offset_returns_self(self): + """set_ragged_offset() returns self for chaining.""" + tensor = hipdnn.Tensor.create([4], hipdnn.DataType.FLOAT) + offset = hipdnn.Tensor.create([4], hipdnn.DataType.INT32) + + assert tensor.set_ragged_offset(offset) is tensor + def test_create_does_not_auto_assign_uid(self): """create() leaves the uid unset until set_uid() is called.""" tensor = hipdnn.Tensor.create([1, 2], hipdnn.DataType.FLOAT) @@ -192,3 +312,120 @@ def test_rejects_unconvertible_value(self): hipdnn.Tensor().set_value(1.5, hipdnn.DataType.INT32) with pytest.raises(TypeError, match="cannot infer"): hipdnn.Tensor().set_value("2.5") + + +class TestScalarValueGetters: + """Getters return the stored scalar as a plain Python value.""" + + @pytest.mark.parametrize( + "data_type, value, python_type", + [ + (hipdnn.DataType.FLOAT, 2.5, float), + (hipdnn.DataType.DOUBLE, 0.1, float), + (hipdnn.DataType.HALF, 2.5, float), + (hipdnn.DataType.BFLOAT16, 2.5, float), + (hipdnn.DataType.UINT8, 200, int), + (hipdnn.DataType.INT32, -7, int), + (hipdnn.DataType.INT64, 1 << 40, int), + (hipdnn.DataType.BOOLEAN, True, bool), + ], + ) + def test_value_variant_returns_plain_python_scalar( + self, data_type, value, python_type + ): + tensor = hipdnn.Tensor().set_value(value, data_type) + + result = tensor.get_value_variant() + + assert type(result) is python_type + assert result == value + + def test_getters_follow_the_scalar_state(self): + tensor = hipdnn.Tensor.create([1], hipdnn.DataType.FLOAT) + + assert tensor.get_value_variant() is None + assert tensor.get_compile_time_constant() is None + assert tensor.get_pass_by_value() is None + + tensor.set_value(2.5) + assert tensor.get_compile_time_constant() == 2.5 + assert tensor.get_pass_by_value() is None + + tensor.set_is_pass_by_value(True) + assert tensor.get_pass_by_value() == 2.5 + assert tensor.get_compile_time_constant() is None + + tensor.clear_value() + assert tensor.get_value_variant() is None + + +class TestSetCompileTimeConstant: + """set_compile_time_constant() is an alias of set_value().""" + + def test_bakes_a_constant_like_set_value(self): + tensor = hipdnn.Tensor() + + assert tensor.set_compile_time_constant(2.5, hipdnn.DataType.HALF) is tensor + assert tensor.get_data_type() == hipdnn.DataType.HALF + assert tensor.get_compile_time_constant() == 2.5 + + def test_type_inferred_from_python_value(self): + tensor = hipdnn.Tensor().set_compile_time_constant(3) + + assert tensor.get_data_type() == hipdnn.DataType.INT64 + + def test_rejects_multi_element_tensor(self): + tensor = hipdnn.Tensor.create([2], hipdnn.DataType.FLOAT) + + with pytest.raises(ValueError, match="one element"): + tensor.set_compile_time_constant(1.0) + + +class TestGraphTensorScalar: + """Graph.tensor_scalar() creates a scalar tensor; the Python value picks the type.""" + + @pytest.mark.parametrize( + "value, data_type", + [ + (0.5, hipdnn.DataType.FLOAT), + (0.1, hipdnn.DataType.DOUBLE), + (3, hipdnn.DataType.INT32), + (2**40, hipdnn.DataType.INT64), + ], + ) + def test_data_type_follows_the_python_value(self, value, data_type): + kind = hipdnn.ScalarType.COMPILE_TIME_CONST + + scalar = hipdnn.Graph.tensor_scalar(value, kind) + + assert scalar.get_data_type() == data_type + + def test_compile_time_constant_keeps_the_value(self): + kind = hipdnn.ScalarType.COMPILE_TIME_CONST + + scalar = hipdnn.Graph.tensor_scalar(0.5, kind) + + assert scalar.get_compile_time_constant() == 0.5 + assert not scalar.get_is_runtime_pass_by_value() + + def test_runtime_param_is_runtime_with_default(self): + kind = hipdnn.ScalarType.RUNTIME_PARAM + + scalar = hipdnn.Graph.tensor_scalar(0.5, kind) + + assert scalar.get_pass_by_value() == 0.5 + assert scalar.get_is_runtime_pass_by_value() + + @pytest.mark.parametrize( + "scalar_type, api", + [ + (hipdnn.ScalarType.COMPILE_TIME_CONST, (1, 0)), + (hipdnn.ScalarType.RUNTIME_PARAM, (1, 2)), + ], + ) + def test_serializes_in_a_graph(self, scalar_type, api): + entry, min_api = _serialized_scalar( + hipdnn.Graph.tensor_scalar(0.5, scalar_type) + ) + + assert (entry["data_type"], entry["value"], min_api) == ("float", 0.5, api)