diff options
author | Håvard Pettersen <havardpe@oath.com> | 2020-09-10 10:34:22 +0000 |
---|---|---|
committer | Håvard Pettersen <havardpe@oath.com> | 2020-09-15 10:47:00 +0000 |
commit | a5e7fb555fc75678ba9a802f01549635895630a4 (patch) | |
tree | d69a67076e877e933ab96bd698aaf0289697037a /eval | |
parent | 0aa542afda6ad9a07c1db760b9a9e3ca3f1b5f86 (diff) |
start work on value api
Diffstat (limited to 'eval')
-rw-r--r-- | eval/CMakeLists.txt | 1 | ||||
-rw-r--r-- | eval/src/tests/eval/simple_value/CMakeLists.txt | 9 | ||||
-rw-r--r-- | eval/src/tests/eval/simple_value/simple_value_test.cpp | 65 | ||||
-rw-r--r-- | eval/src/tests/eval/value_type/value_type_test.cpp | 8 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/CMakeLists.txt | 1 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/simple_value.cpp | 261 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/simple_value.h | 215 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/value_type.cpp | 12 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/value_type.h | 1 |
9 files changed, 573 insertions, 0 deletions
diff --git a/eval/CMakeLists.txt b/eval/CMakeLists.txt index fe9d9985c6a..76c1a63b881 100644 --- a/eval/CMakeLists.txt +++ b/eval/CMakeLists.txt @@ -24,6 +24,7 @@ vespa_define_module( src/tests/eval/node_types src/tests/eval/param_usage src/tests/eval/simple_tensor + src/tests/eval/simple_value src/tests/eval/tensor_function src/tests/eval/tensor_lambda src/tests/eval/tensor_spec diff --git a/eval/src/tests/eval/simple_value/CMakeLists.txt b/eval/src/tests/eval/simple_value/CMakeLists.txt new file mode 100644 index 00000000000..429d3ffaf3d --- /dev/null +++ b/eval/src/tests/eval/simple_value/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(eval_simple_value_test_app TEST + SOURCES + simple_value_test.cpp + DEPENDS + vespaeval + GTest::GTest +) +vespa_add_test(NAME eval_simple_value_test_app COMMAND eval_simple_value_test_app) diff --git a/eval/src/tests/eval/simple_value/simple_value_test.cpp b/eval/src/tests/eval/simple_value/simple_value_test.cpp new file mode 100644 index 00000000000..988e0a453bc --- /dev/null +++ b/eval/src/tests/eval/simple_value/simple_value_test.cpp @@ -0,0 +1,65 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/eval/eval/simple_value.h> +#include <vespa/eval/eval/test/tensor_model.hpp> +#include <vespa/vespalib/gtest/gtest.h> + +using namespace vespalib::eval; +using namespace vespalib::eval::test; + +std::vector<Layout> layouts = { + {}, + {x(3)}, + {x(3),y(5)}, + {x(3),y(5),z(7)}, + float_cells({x(3),y(5),z(7)}), + {x({"a","b","c"})}, + {x({"a","b","c"}),y({"foo","bar"})}, + {x({"a","b","c"}),y({"foo","bar"}),z({"i","j","k","l"})}, + float_cells({x({"a","b","c"}),y({"foo","bar"}),z({"i","j","k","l"})}), + {x(3),y({"foo", "bar"}),z(7)}, + {x({"a","b","c"}),y(5),z({"i","j","k","l"})}, + float_cells({x({"a","b","c"}),y(5),z({"i","j","k","l"})}) +}; + +TEST(SimpleValueTest, simple_values_can_be_converted_from_and_to_tensor_spec) { + for (const auto &layout: layouts) { + TensorSpec expect = spec(layout, N()); + std::unique_ptr<NewValue> value = new_value_from_spec(expect, SimpleValueBuilderFactory()); + TensorSpec actual = spec_from_new_value(*value); + EXPECT_EQ(actual, expect); + } +} + +TEST(SimpleValueTest, simple_value_can_be_built_and_inspected) { + ValueType type = ValueType::from_spec("tensor<float>(x{},y[2],z{})"); + SimpleValueBuilderFactory factory; + std::unique_ptr<ValueBuilder<float>> builder = factory.create_value_builder<float>(type); + float seq = 0.0; + for (vespalib::string x: {"a", "b", "c"}) { + for (vespalib::string y: {"aa", "bb"}) { + auto subspace = builder->add_subspace({x, y}); + EXPECT_EQ(subspace.size(), 2); + subspace[0] = seq + 1.0; + subspace[1] = seq + 5.0; + seq += 10.0; + } + seq += 100.0; + } + std::unique_ptr<NewValue> value = builder->build(std::move(builder)); + EXPECT_EQ(value->index().size(), 6); + auto view = value->index().create_view({0}); + vespalib::stringref query = "b"; + vespalib::stringref label; + size_t subspace; + view->lookup({&query}); + EXPECT_TRUE(view->next_result({&label}, subspace)); + EXPECT_EQ(label, "aa"); + EXPECT_EQ(subspace, 2); + EXPECT_TRUE(view->next_result({&label}, subspace)); + EXPECT_EQ(label, "bb"); + EXPECT_EQ(subspace, 3); + EXPECT_FALSE(view->next_result({&label}, subspace)); +} + +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/eval/src/tests/eval/value_type/value_type_test.cpp b/eval/src/tests/eval/value_type/value_type_test.cpp index 54bcd0694e2..c783c32e699 100644 --- a/eval/src/tests/eval/value_type/value_type_test.cpp +++ b/eval/src/tests/eval/value_type/value_type_test.cpp @@ -315,6 +315,14 @@ TEST("require that type-related predicate functions work as expected") { TEST_DO(verify_predicates(type("tensor<float>(x[5],y{})"), false, false, true, false, false)); } +TEST("require that mapped dimensions can be counted") { + EXPECT_EQUAL(type("double").count_mapped_dimensions(), 0u); + EXPECT_EQUAL(type("tensor(x[5],y[5])").count_mapped_dimensions(), 0u); + EXPECT_EQUAL(type("tensor(x{},y[5])").count_mapped_dimensions(), 1u); + EXPECT_EQUAL(type("tensor(x[5],y{})").count_mapped_dimensions(), 1u); + EXPECT_EQUAL(type("tensor(x{},y{})").count_mapped_dimensions(), 2u); +} + TEST("require that dense subspace size calculation works as expected") { EXPECT_EQUAL(type("error").dense_subspace_size(), 1u); EXPECT_EQUAL(type("double").dense_subspace_size(), 1u); diff --git a/eval/src/vespa/eval/eval/CMakeLists.txt b/eval/src/vespa/eval/eval/CMakeLists.txt index 002a027c3a9..973245607de 100644 --- a/eval/src/vespa/eval/eval/CMakeLists.txt +++ b/eval/src/vespa/eval/eval/CMakeLists.txt @@ -20,6 +20,7 @@ vespa_add_library(eval_eval OBJECT param_usage.cpp simple_tensor.cpp simple_tensor_engine.cpp + simple_value.cpp string_stuff.cpp tensor.cpp tensor_engine.cpp diff --git a/eval/src/vespa/eval/eval/simple_value.cpp b/eval/src/vespa/eval/eval/simple_value.cpp new file mode 100644 index 00000000000..c2e1f2b5adc --- /dev/null +++ b/eval/src/vespa/eval/eval/simple_value.cpp @@ -0,0 +1,261 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "simple_value.h" +#include "tensor_spec.h" +#include <vespa/vespalib/util/stash.h> +#include <vespa/vespalib/util/typify.h> + +#include <vespa/log/log.h> +LOG_SETUP(".eval.simple_value"); + +namespace vespalib::eval { + +//----------------------------------------------------------------------------- + +namespace { + +struct CreateSimpleValueBuilderBase { + template <typename T> static std::unique_ptr<ValueBuilderBase> invoke(const ValueType &type, + size_t num_mapped_in, size_t subspace_size_in) + { + assert(check_cell_type<T>(type.cell_type())); + return std::make_unique<SimpleValueT<T>>(type, num_mapped_in, subspace_size_in); + } +}; + +struct CreateValueFromTensorSpec { + template <typename T> static std::unique_ptr<NewValue> invoke(const ValueType &type, const TensorSpec &spec, const ValueBuilderFactory &factory) { + using SparseKey = std::vector<vespalib::stringref>; + using DenseMap = std::map<size_t,T>; + std::map<SparseKey,DenseMap> map; + for (const auto &entry: spec.cells()) { + SparseKey sparse_key; + size_t dense_key = 0; + for (const auto &dim: type.dimensions()) { + auto pos = entry.first.find(dim.name); + assert(pos != entry.first.end()); + assert(pos->second.is_mapped() == dim.is_mapped()); + if (dim.is_mapped()) { + sparse_key.emplace_back(pos->second.name); + } else { + dense_key = (dense_key * dim.size) + pos->second.index; + } + } + map[sparse_key][dense_key] = entry.second; + } + auto builder = factory.create_value_builder<T>(type, type.count_mapped_dimensions(), type.dense_subspace_size(), map.size()); + for (const auto &entry: map) { + auto subspace = builder->add_subspace(entry.first); + for (const auto &cell: entry.second) { + subspace[cell.first] = cell.second; + } + } + return builder->build(std::move(builder)); + } +}; + +struct CreateTensorSpecFromValue { + template <typename T> static TensorSpec invoke(const NewValue &value) { + auto cells = value.cells().typify<T>(); + TensorSpec spec(value.type().to_spec()); + size_t subspace_id = 0; + size_t subspace_size = value.type().dense_subspace_size(); + std::vector<vespalib::stringref> labels(value.type().count_mapped_dimensions()); + std::vector<vespalib::stringref*> label_refs; + for (auto &label: labels) { + label_refs.push_back(&label); + } + auto view = value.index().create_view({}); + view->lookup({}); + while (view->next_result(label_refs, subspace_id)) { + size_t label_idx = 0; + TensorSpec::Address addr; + for (const auto &dim: value.type().dimensions()) { + if (dim.is_mapped()) { + addr.emplace(dim.name, labels[label_idx++]); + } + } + for (size_t i = 0; i < subspace_size; ++i) { + size_t dense_key = i; + for (auto dim = value.type().dimensions().rbegin(); + dim != value.type().dimensions().rend(); ++dim) + { + if (dim->is_indexed()) { + size_t label = dense_key % dim->size; + addr.emplace(dim->name, label).first->second = TensorSpec::Label(label); + dense_key /= dim->size; + } + } + spec.add(addr, cells[(subspace_size * subspace_id) + i]); + } + } + return spec; + } +}; + +class SimpleValueView : public NewValue::Index::View { +private: + using Addr = std::vector<vespalib::string>; + using Map = std::map<Addr,size_t>; + using Itr = Map::const_iterator; + + const Map &_index; + size_t _num_mapped; + std::vector<size_t> _match_dims; + std::vector<size_t> _extract_dims; + Addr _query; + Itr _pos; + + bool is_direct_lookup() const { return (_match_dims.size() == _num_mapped); } + bool is_match() const { + assert(_pos->first.size() == _num_mapped); + for (size_t idx: _match_dims) { + if (_query[idx] != _pos->first[idx]) { + return false; + } + } + return true; + } + +public: + SimpleValueView(const Map &index, const std::vector<size_t> &match_dims, size_t num_mapped) + : _index(index), _num_mapped(num_mapped), _match_dims(match_dims), _extract_dims(), _query(num_mapped, ""), _pos(_index.end()) + { + auto pos = _match_dims.begin(); + for (size_t i = 0; i < _num_mapped; ++i) { + if ((pos == _match_dims.end()) || (*pos != i)) { + _extract_dims.push_back(i); + } else { + ++pos; + } + } + assert(pos == _match_dims.end()); + assert((_match_dims.size() + _extract_dims.size()) == _num_mapped); + } + + void lookup(const std::vector<const vespalib::stringref*> &addr) override { + assert(addr.size() == _match_dims.size()); + for (size_t i = 0; i < _match_dims.size(); ++i) { + _query[_match_dims[i]] = *addr[i]; + } + if (is_direct_lookup()) { + _pos = _index.find(_query); + } else { + _pos = _index.begin(); + } + } + + bool next_result(const std::vector<vespalib::stringref*> &addr_out, size_t &idx_out) override { + assert(addr_out.size() == _extract_dims.size()); + while (_pos != _index.end()) { + if (is_match()) { + for (size_t i = 0; i < _extract_dims.size(); ++i) { + *addr_out[i] = _pos->first[_extract_dims[i]]; + } + idx_out = _pos->second; + if (is_direct_lookup()) { + _pos = _index.end(); + } else { + ++_pos; + } + return true; + } + ++_pos; + } + return false; + } +}; + +} + +//----------------------------------------------------------------------------- + +void +SimpleValue::add_mapping(const std::vector<vespalib::stringref> &addr) +{ + size_t id = _index.size(); + std::vector<vespalib::string> my_addr; + for (const auto &label: addr) { + my_addr.push_back(label); + } + auto res = _index.emplace(std::move(my_addr), id); + assert(res.second); +} + +SimpleValue::SimpleValue(const ValueType &type, size_t num_mapped_in, size_t subspace_size_in) + : _type(type), + _num_mapped(num_mapped_in), + _subspace_size(subspace_size_in), + _index() +{ + assert(_type.count_mapped_dimensions() == _num_mapped); + assert(_type.dense_subspace_size() == _subspace_size); +} + +SimpleValue::~SimpleValue() = default; + +std::unique_ptr<NewValue::Index::View> +SimpleValue::create_view(const std::vector<size_t> &dims) const +{ + return std::make_unique<SimpleValueView>(_index, dims, _num_mapped); +} + +//----------------------------------------------------------------------------- + +template <typename T> +SimpleValueT<T>::SimpleValueT(const ValueType &type, size_t num_mapped_in, size_t subspace_size_in) + : SimpleValue(type, num_mapped_in, subspace_size_in), + _cells() +{ +} + +template <typename T> +SimpleValueT<T>::~SimpleValueT() = default; + +template <typename T> +ArrayRef<T> +SimpleValueT<T>::add_subspace(const std::vector<vespalib::stringref> &addr) +{ + size_t old_size = _cells.size(); + assert(old_size == (index().size() * subspace_size())); + add_mapping(addr); + _cells.resize(old_size + subspace_size()); + return ArrayRef<T>(&_cells[old_size], subspace_size()); +} + +//----------------------------------------------------------------------------- + +std::unique_ptr<ValueBuilderBase> +SimpleValueBuilderFactory::create_value_builder_base(const ValueType &type, + size_t num_mapped_in, size_t subspace_size_in, size_t) const +{ + return typify_invoke<1,TypifyCellType,CreateSimpleValueBuilderBase>(type.cell_type(), type, num_mapped_in, subspace_size_in); +} + +//----------------------------------------------------------------------------- + +std::unique_ptr<NewValue> new_join(const NewValue &a, const NewValue &b, join_fun_t function, const ValueBuilderFactory &factory) { + (void) a; + (void) b; + (void) function; + (void) factory; + return std::unique_ptr<NewValue>(nullptr); +} + +//----------------------------------------------------------------------------- + +std::unique_ptr<NewValue> new_value_from_spec(const TensorSpec &spec, const ValueBuilderFactory &factory) { + ValueType type = ValueType::from_spec(spec.type()); + assert(!type.is_error()); + return typify_invoke<1,TypifyCellType,CreateValueFromTensorSpec>(type.cell_type(), type, spec, factory); +} + +//----------------------------------------------------------------------------- + +TensorSpec spec_from_new_value(const NewValue &value) { + return typify_invoke<1,TypifyCellType,CreateTensorSpecFromValue>(value.type().cell_type(), value); +} + +//----------------------------------------------------------------------------- + +} diff --git a/eval/src/vespa/eval/eval/simple_value.h b/eval/src/vespa/eval/eval/simple_value.h new file mode 100644 index 00000000000..d080a0e31c2 --- /dev/null +++ b/eval/src/vespa/eval/eval/simple_value.h @@ -0,0 +1,215 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "value.h" +#include "value_type.h" +#include <vespa/eval/tensor/dense/typed_cells.h> +#include <vespa/vespalib/stllike/string.h> +#include <vector> +#include <map> + +namespace vespalib { class Stash; } + +namespace vespalib::eval { + +class TensorSpec; + +using TypedCells = ::vespalib::tensor::TypedCells; + +/** + * Experimental interface layer that will be moved into Value when all + * existing implementations are able to implement it. This interface + * will try to unify scalars, dense tensors, sparse tensors and mixed + * tensors while also enabling operations to be implemented + * efficiently using this interface without having knowledge about the + * actual implementation. Baseline operations will treat all values as + * mixed tensors. Simplified and optimized variants may replace them + * as done today based on type knowledge. + * + * All values are expected to be separated into a continuous area + * storing cells as concatenated dense subspaces, and an index + * structure used to look up label combinations; mapping them into a + * set of dense subspaces. + **/ +struct NewValue : Value { + + // Root lookup structure for mapping labels to dense subspace indexes + struct Index { + + // A view able to look up dense subspace indexes from labels + // specifying a partial address for the dimensions given to + // create_view. A view is re-usable. Lookups are performed by + // calling the lookup function and lookup results are + // extracted using the next_result function. + struct View { + + // look up dense subspace indexes from labels specifying a + // partial address for the dimensions given to + // create_view. Results from the lookup is extracted using + // the next_result function. + virtual void lookup(const std::vector<const vespalib::stringref*> &addr) = 0; + + // Extract the next result (if any) from the previous + // lookup into the given partial address and index. Only + // the labels for the dimensions NOT specified in + // create_view will be extracted here. + virtual bool next_result(const std::vector<vespalib::stringref*> &addr_out, size_t &idx_out) = 0; + + virtual ~View() {} + }; + + // total number of mappings (equal to the number of dense subspaces) + virtual size_t size() const = 0; + + // create a view able to look up dense subspaces based on + // labels from a subset of the mapped dimensions. + virtual std::unique_ptr<View> create_view(const std::vector<size_t> &dims) const = 0; + + virtual ~Index() {} + }; + virtual TypedCells cells() const = 0; + virtual const Index &index() const = 0; + virtual ~NewValue() {} +}; + +/** + * Tagging interface used as return type from factories before + * downcasting to actual builder with specialized cell type. + **/ +struct ValueBuilderBase { + virtual ~ValueBuilderBase() {} +}; + +/** + * Interface used to build a value one dense subspace at a + * time. Enables decoupling of what the value should contain from how + * to store the value. + **/ +template <typename T> +struct ValueBuilder : ValueBuilderBase { + // add a dense subspace for the given address (label for all + // mapped dimensions in canonical order). Note that previously + // returned subspaces will be invalidated when new subspaces are + // added. Also note that adding the same subspace multiple times + // is not allowed. + virtual ArrayRef<T> add_subspace(const std::vector<vespalib::stringref> &addr) = 0; + + // Given the ownership of the builder itself, produce the newly + // created value. This means that builders can only be used once, + // it also means values can build themselves. + virtual std::unique_ptr<NewValue> build(std::unique_ptr<ValueBuilder> self) = 0; +}; + +/** + * Factory able to create appropriate value builders. We do not really + * care about the full mathematical type here, but it needs to be + * passed since it is exposed in the value api. The expected number of + * subspaces is also passed since it enables the builder to pre-size + * internal structures appropriately. Note that since we are not able + * to have virtual templated functions we need to cast the created + * builder. With interoperability between all values. + **/ +struct ValueBuilderFactory { + template <typename T> + std::unique_ptr<ValueBuilder<T>> create_value_builder(const ValueType &type, + size_t num_mapped_in, size_t subspace_size_in, size_t expect_subspaces) const + { + assert(check_cell_type<T>(type.cell_type())); + auto base = create_value_builder_base(type, num_mapped_in, subspace_size_in, expect_subspaces); + ValueBuilder<T> *builder = dynamic_cast<ValueBuilder<T>*>(base.get()); + assert(builder); + base.release(); + return std::unique_ptr<ValueBuilder<T>>(builder); + } + template <typename T> + std::unique_ptr<ValueBuilder<T>> create_value_builder(const ValueType &type) const + { + return create_value_builder<T>(type, type.count_mapped_dimensions(), type.dense_subspace_size(), 1); + } + virtual ~ValueBuilderFactory() {} +protected: + virtual std::unique_ptr<ValueBuilderBase> create_value_builder_base(const ValueType &type, + size_t num_mapped_in, size_t subspace_size_in, size_t expect_subspaces) const = 0; +}; + +/** + * A simple implementation of a generic value that can also be used to + * build new values. This class focuses on simplicity over speed and + * is intended as a reference implementation that can also be used to + * test the correctness of tensor operations as they are moved away + * from the implementation of individual tensor classes. + **/ +class SimpleValue : public NewValue, public NewValue::Index +{ +private: + using Addr = std::vector<vespalib::string>; + ValueType _type; + size_t _num_mapped; + size_t _subspace_size; + std::map<Addr,size_t> _index; +protected: + size_t subspace_size() const { return _subspace_size; } + void add_mapping(const std::vector<vespalib::stringref> &addr); +public: + SimpleValue(const ValueType &type, size_t num_mapped_in, size_t subspace_size_in); + ~SimpleValue() override; + const ValueType &type() const override { return _type; } + const NewValue::Index &index() const override { return *this; } + size_t size() const override { return _index.size(); } + std::unique_ptr<View> create_view(const std::vector<size_t> &dims) const override; +}; + +/** + * Subclasses of SimpleValue handling cell type specialization. + **/ +template <typename T> +class SimpleValueT : public SimpleValue, public ValueBuilder<T> +{ +private: + std::vector<T> _cells; +public: + SimpleValueT(const ValueType &type, size_t num_mapped_in, size_t subspace_size_in); + ~SimpleValueT() override; + TypedCells cells() const override { return TypedCells(ConstArrayRef<T>(_cells)); } + ArrayRef<T> add_subspace(const std::vector<vespalib::stringref> &addr) override; + std::unique_ptr<NewValue> build(std::unique_ptr<ValueBuilder<T>> self) override { + ValueBuilder<T>* me = this; + assert(me == self.get()); + self.release(); + return std::unique_ptr<NewValue>(this); + } +}; + +/** + * ValueBuilderFactory implementation for SimpleValue. + **/ +struct SimpleValueBuilderFactory : ValueBuilderFactory { + ~SimpleValueBuilderFactory() override {} +protected: + std::unique_ptr<ValueBuilderBase> create_value_builder_base(const ValueType &type, + size_t num_mapped_in, size_t subspace_size_in, size_t expect_subspaces) const override; +}; + +/** + * Generic join operation treating both values as mixed + * tensors. Packaging will change, and while the baseline join will + * not have information about low-level value class implementations, + * it will have up-front knowledge about types, specifically + * dimensional overlap and result type. + **/ +using join_fun_t = double (*)(double, double); +std::unique_ptr<NewValue> new_join(const NewValue &a, const NewValue &b, join_fun_t function, const ValueBuilderFactory &factory); + +/** + * Make a value from a tensor spec using a value builder factory + * interface, making it work with any value implementation. + **/ +std::unique_ptr<NewValue> new_value_from_spec(const TensorSpec &spec, const ValueBuilderFactory &factory); + +/** + * Convert a generic value to a tensor spec. + **/ +TensorSpec spec_from_new_value(const NewValue &value); + +} diff --git a/eval/src/vespa/eval/eval/value_type.cpp b/eval/src/vespa/eval/eval/value_type.cpp index 2287e8599eb..2b24a88f473 100644 --- a/eval/src/vespa/eval/eval/value_type.cpp +++ b/eval/src/vespa/eval/eval/value_type.cpp @@ -174,6 +174,18 @@ ValueType::is_dense() const } size_t +ValueType::count_mapped_dimensions() const +{ + size_t cnt = 0; + for (const auto &dim : dimensions()) { + if (dim.is_mapped()) { + ++cnt; + } + } + return cnt; +} + +size_t ValueType::dense_subspace_size() const { size_t size = 1; diff --git a/eval/src/vespa/eval/eval/value_type.h b/eval/src/vespa/eval/eval/value_type.h index a3dbb901eb0..b89c5859593 100644 --- a/eval/src/vespa/eval/eval/value_type.h +++ b/eval/src/vespa/eval/eval/value_type.h @@ -60,6 +60,7 @@ public: bool is_tensor() const { return (_type == Type::TENSOR); } bool is_sparse() const; bool is_dense() const; + size_t count_mapped_dimensions() const; size_t dense_subspace_size() const; const std::vector<Dimension> &dimensions() const { return _dimensions; } std::vector<Dimension> nontrivial_dimensions() const; |