12 мгновений опечаток и копипаста, или почему сбоит AI: проверяем код OpenVINO

«OpenVINO — набор инструментов, позволяющий проводить глубокое обучение AI для взаимодействия с реальным миром теперь Open Source!» — эта новость для нас прозвучала как призыв к действию. Код проекта проверен, ошибки найдены, и первая часть статьи готова к прочтению. Будет интересно!

4f900de3fea2eec6fa36632ce0cb36f5.png

Несколько слов о проекте

6 марта 2024 года Intel выпустила Open Source решение OpenVINO. Что же оно из себя представляет? Прежде всего, это инструмент, помогающий специалистам более эффективно обучать AI компьютерному зрению.

А если не знаете, что такое компьютерное зрение, вам сюда.

Компьютерное зрение — это когда с помощью более или менее нормальной камеры, подключённой к компьютеру, мы можем распознать на видео какие-то необходимые нам данные. Например, мы хотим поймать в объектив белого кролика посреди таких же белых сугробов зимой. Глазами это будет сделать крайне проблематично, но с помощью камеры и обученного AI любой кролик, пробегающий мимо, не останется незамеченным.

Стоит также отметить, что компьютерное зрение штука крайне полезная, и помогает во многих областях, например:

  1. Сортировка и классификация сельскохозяйственных продуктов;

  2. Классификация и выявления дефектов на производстве у различных электронных модулей и компонентов;

  3. Промышленная автоматизация;

  4. Системы распознавания лиц;

  5. Использование AI в колоноскопии, при ухудшении зрения или же при выявлении заболеваний мозга.

Это не полный список возможностей, но как бы и статья не про это. В любом случае, полезность использования технологий искусственного интеллекта неоспорима, и для того, чтобы всё это стало возможным, разрабатываются различные инструменты углублённого обучения. Одним из таких инструментов как раз-таки и является OpenVINO.

Если точнее, OpenVINO — это бесплатный набор готовых утилит, которые разработчики могут использовать для ускорения разработки подобных решений. Так же, помимо инструментов обучения, есть ещё инструменты для тестирования эффективности AI и свой бенчмарк. Как-то так.

И вот, в робкой попытке прикоснуться к прекрасному своими кривульками, я вновь расчехлил статический анализатор PVS-Studio, дабы окунуться в код проекта для поиска инородных сущностей, называемых багами. Как я уже написал ранее, будет интересно.

Однако, несмотря на баги, инструмент свои функции выполняет: AI обучается и работает (если судить по многочисленным статьям разработчиков).

Нижеописанной ситуации в практике обучения своего AI вы не встретите:

Разработчик: AI, я компьютерного зрения не чувствую!

AI: Мяу?!

Коммит, на котором я собирал проект для проверки: 2d8ac08.

Дисклеймер

Написание и публикация этой статьи не имеют цели обесценить труд программистов, занимающихся разработкой данного продукта. Цель: популяризация статических анализаторов кода, которые полезны даже для качественных и состоявшихся проектов.

Результаты проверки

Разбираемый проект оказался богат на опечатки и проблемы, которые возникают после copy-paste.

Исходя из этого и было принято решение написать отдельную статью, дабы подчеркнуть тот факт, что ошибки такого рода встречаются практически везде.

И да, всегда есть вероятность того, что так и было задумано программистом, но, сия вероятность… крайне мала: «Иммолейт импрувед!»

Фрагмент N1

ov::pass::ConvertPadToGroupConvolution::
                       ConvertPadToGroupConvolution() 
{
  ....
  const auto& pad_begin = pad->get_pads_begin();
  const auto& pad_end = pad->get_pads_end();

  if (pad_begin.empty() || pad_end.empty()) 
  {
    // pads will be empty if inputs are not constants
    return false;
  }

  // check that Pad has non-negative values
  auto pred = [](int64_t a) 
              {
                return a < 0;
              };
  if (std::any_of(pad_begin.begin(), pad_begin.end(), pred) ||
      std::any_of(pad_begin.begin(), pad_begin.end(), pred))  // <=
  { 
    return false;
  }
  ....
}

Предупреждение анализатора:

V501 There are identical sub-expressions 'std: any_of (pad_begin.begin (), pad_begin.end (), pred)' to the left and to the right of the '||' operator. convert_pad_to_group_conv.cpp 66

Как вы можете заметить, в самом нижнем условии сравниваются два одинаковых выражения: одни и те же вызовы функции std: any_of с одинаковыми параметрами. Код выше добавлен в пример не просто так, ведь если посмотреть на него, можно сделать вывод, что условие, скорее всего, должно быть написано следующим образом:

if (std::any_of(pad_begin.begin(), pad_begin.end(), pred) ||
    std::any_of(pad_end.begin(), pad_end.end(), pred)) 
{    
  return false;
}

Фрагмент N2

ov::pass::ShuffleChannelsFusion::
                   ShuffleChannelsFusion(const bool reshape_constants_check)
{
  ....
  auto reshape_before_constant = std::dynamic_pointer_cast
                                       (
       pattern_map.at(reshape_before_const_pattern).get_node_shared_ptr());

  auto reshape_before = std::dynamic_pointer_cast(
       pattern_map.at(reshape_before_pattern).get_node_shared_ptr());

  auto transpose = std::dynamic_pointer_cast(
       pattern_map.at(transpose_pattern).get_node_shared_ptr());

  auto reshape_after = std::dynamic_pointer_cast(
       pattern_map.at(reshape_after_pattern).get_node_shared_ptr());

  auto reshape_after_constant = std::dynamic_pointer_cast(
       pattern_map.at(reshape_after_const_pattern).get_node_shared_ptr());

  if (!reshape_after || !transpose || !reshape_after ||      // <=
      !reshape_before_constant || !reshape_after_constant) 
  {
    return false;
  }
  ....
}

Предупреждение анализатора:

V501 There are identical sub-expressions '! reshape_after' to the left and to the right of the '||' operator. shuffle_channels_fusion.cpp 115

Снова ошибка в условии. Два раза проверяется одно и то же выражение, а именно, что указатель reshape_after ненулевой. Если снова посмотрим на код выше, заметим инициализацию reshape_before. Скорее всего, условие должно быть переписано следующим образом:

if (!reshape_after || !transpose || !reshape_before ||
    !reshape_before_constant || !reshape_after_constant) 
{
    return false;
}

Фрагмент N3

....
using PatternValueMaps = std::vector;
....
PatternValueMaps m_pattern_value_maps;
....
MatcherState::~MatcherState() 
{
  if (m_restore) 
  {
    if (!m_matcher->m_matched_list.empty())
    {
      m_matcher->m_matched_list.erase(m_matcher->m_matched_list.begin() + 
                                      m_watermark,
                                      m_matcher->m_matched_list.end());
    }
    if (!m_pattern_value_maps.empty())
    {
      m_matcher->m_pattern_value_maps.erase(m_pattern_value_maps.begin() + // <=
                                            m_capture_size,
                                            m_pattern_value_maps.end());
    }
    m_matcher->m_pattern_map = m_pattern_value_map;
  }
}

Предупреждение анализатора:

V539 [CERT-CTR53-CPP] Consider inspecting iterators which are being passed as arguments to function 'erase'. matcher.cpp 48

На этот раз ошибка комплексная. Рассмотрим всё по порядку.

Вначале проверяется, что вектор m_pattern_value_maps не пустой.

Затем мы замечаем, что в then-ветке второго вложенного if почему-то происходит работа уже с другим вектором:* m_matcher→m_pattern_value_maps*. Дальше — больше.

В функцию-член std: vector:: erase контейнера m_matcher→m_pattern_value_maps передаются аргументы в виде итераторов другого контейнера — m_pattern_value_maps. И работать это корректно не будет.

Судя по конструктору и деструктору, класс MatcherState предназначен для отката изменений объекта типа Matcher. Возможно, раньше код RAII-обёртки сохранял состояние объекта типа Matcher в полях MatcherState: m_pattern_value_map и MatcherState: m_pattern_value_maps и затем возвращал в деструкторе.

Однако потом код переписали, добавив поля MatcherState: m_watermark и MatcherState: m_capture_size. Они ответственны за удаление элементов, которые были добавлены в конец контейнеров Matcher: m_matched_list и Matcher: m_pattern_value_maps.

Исходя из предыдущих соображений, код нужно поправить следующим образом:

if (!m_matcher->m_pattern_value_maps.empty())
{
  m_matcher->m_pattern_value_maps.erase(
                                  m_matcher->m_pattern_value_maps.begin() +
                                  m_capture_size,
                                  m_matcher->m_pattern_value_maps.end()     );
}

Также хочу отметить, что поле MatcherState: m_pattern_value_maps теперь не используется, и, возможно, его стоит удалить.

Фрагмент N4

template 
void jit_power_dynamic_emitter::
              emit_isa(const std::vector &in_vec_idxs, 
                       const std::vector &out_vec_idxs) const 
{
  ....
  if (isa == x64::avx512_core || isa == x64::avx512_core)    // <=
  {
    h->sub(h->rsp, n_k_regs_to_save * k_mask_size);
    for (size_t i = 0; i < n_k_regs_to_save; ++i) 
    {
      if (x64::mayiuse(x64::avx512_core))
        h->kmovq(h->ptr[h->rsp + i * k_mask_size], Opmask(i));
      else
        h->kmovw(h->ptr[h->rsp + i * k_mask_size], Opmask(i));
    }
  }
  ....
}

Предупреждение анализатора:

V501 There are identical sub-expressions 'isa == x64:: avx512_core' to the left and to the right of the '||' operator. jit_eltwise_emitters.cpp 705

И снова ошибка в условии: проверяются два абсолютно одинаковых выражения. Скорее всего, во втором выражении isa должно быть равно другому значению.

Примечательно, что эта ошибка встречается в коде дополнительно в трёх местах:

  1. V501 There are identical sub-expressions 'isa == x64:: avx512_core' to the left and to the right of the '||' operator. jit_eltwise_emitters.cpp 754

  2. V501 There are identical sub-expressions 'isa == x64:: avx512_core' to the left and to the right of the '||' operator. jit_eltwise_emitters.cpp 1609

  3. V501 There are identical sub-expressions 'isa == x64:: avx512_core' to the left and to the right of the '||' operator. jit_eltwise_emitters.cpp 1658

Фрагмент N5

void GridSampleKernel::reflectionPadding(const Vmm& vCoordDst, 
                                              const Vmm& vCoordOrigin, 
                                              const coord dim         ) 
{
  ....
  if (dim == coord::w) 
  {
    ....
  } else if (coord::h)                      // <=
  {
    ....         
  } else {....}
  ....
}

Предупреждение анализатора:

V768 The enumeration constant 'h' is used as a variable of a Boolean-type. grid_sample.cpp 925

Очень странно, что во втором условии проверяется константа coord: h. Конечно, она имеет значение 1, и такой код всегда будет возвращать true, что явно является ошибкой.

В этом случае код в теле последней ветки else никогда не будет выполнен. Что же это: хитрая задумка разработчика или, может, искусственное ограничение для выполняемого кода? Скорее похоже на баг, и в условии должно быть выражение dim == coord: h.

И ещё несколько подобных предупреждений:

  1. V768 The enumeration constant 'h' is used as a variable of a Boolean-type. grid_sample.cpp 959

  2. V768 The enumeration constant 'h' is used as a variable of a Boolean-type. grid_sample.cpp 990

Ошибка также является следствием того, что программист копировал поле coord: h в условие, но забыл сравнить его значение со значением параметра dim.

Фрагмент N6

OutputVector translate_im2col(const NodeContext& context) 
{
  num_inputs_check(context, 5, 5);
  auto input = context.get_input(0);
  auto kernel_size = context.const_input>(1);
  PYTORCH_OP_CONVERSION_CHECK(kernel_size.size() == 2, 
                              "kernel size should contains 2 elements");
  auto dilation = context.const_input>(2);
  PYTORCH_OP_CONVERSION_CHECK(kernel_size.size() == 2,               // <=
                              "dilation should contains 2 elements");
  auto padding = context.const_input>(3);
  PYTORCH_OP_CONVERSION_CHECK(kernel_size.size() == 2,               // <=
                              "padding should contains 2 elements");
  auto stride = context.const_input>(4);
  PYTORCH_OP_CONVERSION_CHECK(kernel_size.size() == 2,               // <=
                              "stride should contains 2 elements");
  ....
}

Предупреждение анализатора:

V547 Expression 'kernel_size.size () == 2' is always true. im2col.cpp 65

И ещё несколько предупреждений для понимания полной картины:

  1. V547 Expression 'kernel_size.size () == 2' is always true. im2col.cpp 67

  2. V547 Expression 'kernel_size.size () == 2' is always true. im2col.cpp 69

На первый взгляд непонятно, что пытается сказать нам анализатор всеми этими предупреждениями, но давайте порассуждаем.

Первое, что бросается в глаза: проверка kernel_size.size () == 2 происходит аж целых четыре раза. При этом после первой проверки вектор kernel_size нигде не изменяется. На это нам анализатор и намекает, говоря о том, что следующие три проверки всегда истинны.

А как именно анализатор это понял?

В макросе PYTORCH_OP_CONVERSION_CHECK есть функция create, которая бросает исключение, если переданное в макросе выражение ложно. Следовательно, чтобы весь код после первой проверки был достижим (а по умолчанию мы считаем его таковым), необходимо чтобы выражение kernel_size.size () было равно 2.

Следующее, о чём мы могли бы задуматься:, а зачем нам вообще проверять значение kernel_size.size (), если вектор kernel_size не меняется, и его размер всегда будет равен 2? Всё, что мы рассмотрели ранее, было только следствием допущенной ошибки, а не причиной. Причина же проста и заключается в следующем.

Вот был создан и инициализирован объект kernel_size, и следом в макрос PYTORCH_OP_CONVERSION_CHECK передано и проверено внутри выражение kernel_size.size () == 2. Следом создан ещё один объект dilation, однако в макрос PYTORCH_OP_CONVERSION_CHECK так же передано выражение kernel_size.size () == 2, хотя, если подумать, по логике, должно быть передано и проверено выражение dilation.size () == 2.

Так, же, если обратите внимание на второй аргумент, передающийся в этот макрос — строку, то, опять же, становится очевидно, что функция size должна вызываться для объекта dilation. И так же для двух других объектов, ну вы поняли… программист копипастил строки кода и забыл поменять имена для объектов, проверки размера для которых передавал параметром в макрос.

Фрагмент N7

void BinaryConvolution::createPrimitive() 
{
  ....
   bool args_ok = jcp.l_pad <= jcp.ur_w &&                                 // <=
          (r_pad_no_tail <= jcp.ur_w) && (jcp.l_pad <= jcp.ur_w) &&        // <=
            IMPLICATION(jcp.kw > 7, (jcp.t_pad == 0 && jcp.l_pad == 0) ||
              (jcp.stride_w == 1 && jcp.stride_h == 1));
  ....
}

Предупреждение анализатора:

V501 There are identical sub-expressions 'jcp.l_pad <= jcp.ur_w' to the left and to the right of the '&&' operator. bin_conv.cpp 1088

Как мы можем наблюдать, проверяется одно и то же выражение. Ничего плохого не произойдёт, но одно лишнее выражение нужно убрать.

Фрагмент N8

void FakeQuantize::getSupportedDescriptors() 
{
  ....
  if (getInputShapeAtPort(0).getRank() != 
                       getInputShapeAtPort(0).getRank())    // <=
  {
    OPENVINO_THROW(errorPrefix, 
                   "has different ranks for input and output tensors");
  }
  ....
}

Предупреждение анализатора:

V501 There are identical sub-expressions 'getInputShapeAtPort (0).getRank ()' to the left and to the right of the '!=' operator. fake_quantize.cpp 1301

В условии проверяется на неравенство одно и то же подвыражение. Скорее всего, здесь замешана проблема копипаста. А так как нам прямым текстом пишут:»has different ranks for input and output tensors», — и если учесть тот факт, что существует аналогичная функция, но для output значения:

const Shape& getOutputShapeAtPort(size_t port) const 
{
  if (outputShapes.size() <= port) 
  {
    OPENVINO_THROW("Incorrect output port number for node ", getName());
  }
  return outputShapes[port];
}

Скорее всего, условие нужно исправить следующим образом:

if (getInputShapeAtPort(0).getRank() != getOutputShapeAtPort(0).getRank())
{
  OPENVINO_THROW(errorPrefix, 
                 "has different ranks for input and output tensors");
}

Фрагмент N9

void set_state(const ov::SoPtr& state) override 
{
  OPENVINO_ASSERT(state->get_shape() == 
                           m_state->get_shape(),
                              "Wrong tensor shape.");
  OPENVINO_ASSERT(state->get_element_type() == 
                           state->get_element_type(),                 // <=
                              "Wrong tensor type."   );
  OPENVINO_ASSERT(state->get_byte_size() == 
                           state->get_byte_size(),                    // <=
                              "Blob size of tensors are not equal.");
  std::memcpy(m_state->data(), state->data(), state->get_byte_size());
}

Предупреждения анализатора:

  1. V501 There are identical sub-expressions 'state→get_element_type ()' to the left and to the right of the '==' operator. variable_state.hpp 23

  2. V501 There are identical sub-expressions 'state→get_byte_size ()' to the left and to the right of the '==' operator. variable_state.hpp 24

В первой строке сравниваются выражения state→get_shape () и m_state→get_shape (). Однако в следующих строках из-за copy-paste сравниваются результаты вызова функций-членов get_element_type и get_byte_size одного и того же объекта state. Скорее всего, так получилось потому, что имена m_state и state схожи меж собой, и программист не обратил на это внимания.

Исправим код:

....
OPENVINO_ASSERT(state->get_element_type() == 
                         m_state->get_element_type(),
                            "Wrong tensor type."     );
OPENVINO_ASSERT(state->get_byte_size() == 
                         m_state->get_byte_size(),
                            "Blob size of tensors are not equal.");
....

Фрагмент N10

void SubgraphExtractor::add_new_inputs(const std::vector<
                                                InputEdge>& new_inputs,
                                       const bool merge_inputs         ) 
{
  ....
  auto it = std::find_if(new_inputs.begin(), new_inputs.begin(),
                 [&](const InputEdge& input_edge) 
                 {
                   return get_input_tensor_name(m_onnx_graph, 
                                       input_edge) == input.first;
                 }                                                );
  ....
}

Предупреждение анализатора:

V539 [CERT-CTR53-CPP] Consider inspecting iterators which are being passed as arguments to function 'find_if'. subgraph_extraction.cpp 300

Если мы обратим внимание на вызов функции std: find_if, то заметим, что вторым аргументом должен быть вызов new_inputs.end (). В текущем состоянии код всегда будет возвращать new_inputs.begin ().

Исправим код:

....
auto it = std::find_if(new_inputs.begin(), new_inputs.end(),
                       [&](const InputEdge& input_edge) 
                       {
                         return get_input_tensor_name(
                                   m_onnx_graph, input_edge) == input.first;
                       });
....

Фрагмент N11

std::vector inConfs;
....
MemoryDescPtr Node::getBaseMemDescAtInputPort(size_t portNum) const 
{
  if (auto primDesc = getSelectedPrimitiveDescriptor()) 
  {
    const auto& inConfs = primDesc->getConfig().inConfs;
    if (inConfs.size() < portNum)                                // N1
    {                       
      OPENVINO_THROW("Can't get input memory desc at port: ",
                      portNum, ", incorrect port number"     );
    }
    return inConfs[portNum].getMemDesc();                        // N2
  }
  OPENVINO_THROW("Can't get input memory desc, 
                         primitive descriptor is not selected");
}

Предупреждение анализатора:

V557 [CERT-ARR30-C] Array overrun is possible. The 'portNum' index is pointing beyond array bound. node.cpp 402

На первый взгляд, в этом фрагменте кода нет ничего не обычного, и кажется, что анализатор ругается зря. Но нет. Чтобы было проще объяснить, я отметил строки, на которые нужно обратить внимание.

В строке N1 в условии проверяется выражение inConfs.size () < portNum. Условие становится false, когда portNum <= inConfs.size(). Затем в строке N2 происходит доступ к контейнеру inConfs. Доступ к нему должен происходить по индексам в диапазоне [0 … N — 1]. Однако в граничном случае, когда portNum == inConfs.size (), произойдёт выход за границу контейнера, что ведёт к неопределённому поведению.

Верная проверка должна выглядеть следующим образом:

if (portNum >= inConfs.size()) { .... }

Я также поменял операнды местами, поскольку, по моему скромному мнению, читается такая проверка человеком проще.

Прочитав об этой ошибке, читатель может вызвать пояснительную бригаду: «В этом примере нет ни опечаток, ни проблем с copy-paste, тогда что он здесь делает?» Дело в том, что эту ошибку размножили:

....
std::vector outConfs;
....
MemoryDescPtr Node::getBaseMemDescAtOutputPort(size_t portNum) const 
{
  if (auto primDesc = getSelectedPrimitiveDescriptor()) 
  {
    const auto& outConfs = primDesc->getConfig().outConfs;
    if (outConfs.size() < portNum)                               // <=
    {
      OPENVINO_THROW("Can't get output memory desc at port: ", 
                      portNum, ", incorrect port number"      );
    }
    return outConfs[portNum].getMemDesc();                       // <=
  }
  OPENVINO_THROW("Can't get output memory desc, 
                      primitive descriptor is not selected");
}

Предупреждение анализатора:

V557 [CERT-ARR30-C] Array overrun is possible. The 'portNum' index is pointing beyond array bound. node.cpp 413

Код этих двух функций практически совпадает. Очень похоже на то, что одну из них написали путём полного копирования другой, и затем сделали небольшие изменения: переименовали локальную переменную и воспользовались другим полем объекта, поменяли сообщения при бросках исключений. Но ошибка при этом также перекочевала и во вторую функцию.

Поправим и её:

if (portNum >= outConfs.size()) { .... }

А вообще, здесь так и напрашивается рефакторинг: код из этих двух функций стоит выделить в одну общую и затем переиспользовать её.

Фрагмент N12

А сейчас сыграем в мини-игру: вам надо найти опечатки в коде проекта OpenVINO. Кто нашёл, тот молодец! Кто не нашёл, тоже молодцы! Однако, надеюсь, всем стало очевидно — этот пример является одной большой причиной использоваться статический анализатор.

Найдите здесь опечатки:

template 
using caseless_unordered_map = std::unordered_map, CaselessEq>;
using TypeToNameMap = ov::intel_cpu::
                          caseless_unordered_map;

static const TypeToNameMap& get_type_to_name_tbl() {
    static const TypeToNameMap type_to_name_tbl = {
        {"Constant", Type::Input},
        {"Parameter", Type::Input},
        {"Result", Type::Output},
        {"Eye", Type::Eye},
        {"Convolution", Type::Convolution},
        {"GroupConvolution", Type::Convolution},
        {"MatMul", Type::MatMul},
        {"FullyConnected", Type::FullyConnected},
        {"MaxPool", Type::Pooling},
        {"AvgPool", Type::Pooling},
        {"AdaptiveMaxPool", Type::AdaptivePooling},
        {"AdaptiveAvgPool", Type::AdaptivePooling},
        {"Add", Type::Eltwise},
        {"IsFinite", Type::Eltwise},
        {"IsInf", Type::Eltwise},
        {"IsNaN", Type::Eltwise},
        {"Subtract", Type::Eltwise},
        {"Multiply", Type::Eltwise},
        {"Divide", Type::Eltwise},
        {"SquaredDifference", Type::Eltwise},
        {"Maximum", Type::Eltwise},
        {"Minimum", Type::Eltwise},
        {"Mod", Type::Eltwise},
        {"FloorMod", Type::Eltwise},
        {"Power", Type::Eltwise},
        {"PowerStatic", Type::Eltwise},
        {"Equal", Type::Eltwise},
        {"NotEqual", Type::Eltwise},
        {"Greater", Type::Eltwise},
        {"GreaterEqual", Type::Eltwise},
        {"Less", Type::Eltwise},
        {"LessEqual", Type::Eltwise},
        {"LogicalAnd", Type::Eltwise},
        {"LogicalOr", Type::Eltwise},
        {"LogicalXor", Type::Eltwise},
        {"LogicalNot", Type::Eltwise},
        {"Relu", Type::Eltwise},
        {"LeakyRelu", Type::Eltwise},
        {"Gelu", Type::Eltwise},
        {"Elu", Type::Eltwise},
        {"Tanh", Type::Eltwise},
        {"Sigmoid", Type::Eltwise},
        {"Abs", Type::Eltwise},
        {"Sqrt", Type::Eltwise},
        {"Clamp", Type::Eltwise},
        {"Exp", Type::Eltwise},
        {"SwishCPU", Type::Eltwise},
        {"HSwish", Type::Eltwise},
        {"Mish", Type::Eltwise},
        {"HSigmoid", Type::Eltwise},
        {"Round", Type::Eltwise},
        {"PRelu", Type::Eltwise},
        {"Erf", Type::Eltwise},
        {"SoftPlus", Type::Eltwise},
        {"SoftSign", Type::Eltwise},
        {"Select", Type::Eltwise},
        {"Log", Type::Eltwise},
        {"BitwiseAnd", Type::Eltwise},
        {"BitwiseNot", Type::Eltwise},
        {"BitwiseOr", Type::Eltwise},
        {"BitwiseXor", Type::Eltwise},
        {"Reshape", Type::Reshape},
        {"Squeeze", Type::Reshape},
        {"Unsqueeze", Type::Reshape},
        {"ShapeOf", Type::ShapeOf},
        {"NonZero", Type::NonZero},
        {"Softmax", Type::Softmax},
        {"Reorder", Type::Reorder},
        {"BatchToSpace", Type::BatchToSpace},
        {"SpaceToBatch", Type::SpaceToBatch},
        {"DepthToSpace", Type::DepthToSpace},
        {"SpaceToDepth", Type::SpaceToDepth},
        {"Roll", Type::Roll},
        {"LRN", Type::Lrn},
        {"Split", Type::Split},
        {"VariadicSplit", Type::Split},
        {"Concat", Type::Concatenation},
        {"ConvolutionBackpropData", Type::Deconvolution},
        {"GroupConvolutionBackpropData", Type::Deconvolution},
        {"StridedSlice", Type::StridedSlice},
        {"Slice", Type::StridedSlice},
        {"Tile", Type::Tile},
        {"ROIAlign", Type::ROIAlign},
        {"ROIPooling", Type::ROIPooling},
        {"PSROIPooling", Type::PSROIPooling},
        {"DeformablePSROIPooling", Type::PSROIPooling},
        {"Pad", Type::Pad},
        {"Transpose", Type::Transpose},
        {"LSTMCell", Type::RNNCell},
        {"GRUCell", Type::RNNCell},
        {"AUGRUCell", Type::RNNCell},
        {"RNNCell", Type::RNNCell},
        {"LSTMSequence", Type::RNNSeq},
        {"GRUSequence", Type::RNNSeq},
        {"AUGRUSequence", Type::RNNSeq},
        {"RNNSequence", Type::RNNSeq},
        {"FakeQuantize", Type::FakeQuantize},
        {"BinaryConvolution", Type::BinaryConvolution},
        {"DeformableConvolution", Type::DeformableConvolution},
        {"TensorIterator", Type::TensorIterator},
        {"Loop", Type::TensorIterator},
        {"ReadValue", Type::MemoryInput},  // for construction from name
                                           // ctor, arbitrary name is used
        {"Assign", Type::MemoryOutput},    // for construction from layer ctor
        {"Convert", Type::Convert},
        {"NV12toRGB", Type::ColorConvert},
        {"NV12toBGR", Type::ColorConvert},
        {"I420toRGB", Type::ColorConvert},
        {"I420toBGR", Type::ColorConvert},
        {"MVN", Type::MVN},
        {"NormalizeL2", Type::NormalizeL2},
        {"ScatterUpdate", Type::ScatterUpdate},
        {"ScatterElementsUpdate", Type::ScatterElementsUpdate},
        {"ScatterNDUpdate", Type::ScatterNDUpdate},
        {"Interpolate", Type::Interpolate},
        {"RandomUniform", Type::RandomUniform},
        {"ReduceL1", Type::Reduce},
        {"ReduceL2", Type::Reduce},
        {"ReduceLogicalAnd", Type::Reduce},
        {"ReduceLogicalOr", Type::Reduce},
        {"ReduceMax", Type::Reduce},
        {"ReduceMean", Type::Reduce},
        {"ReduceMin", Type::Reduce},
        {"ReduceProd", Type::Reduce},
        {"ReduceSum", Type::Reduce},  
        {"ReduceLogSum", Type::Reduce},
        {"ReduceLogSumExp", Type::Reduce},
        {"ReduceSumSquare", Type::Reduce},
        {"Broadcast", Type::Broadcast},  
        {"EmbeddingSegmentsSum", Type::EmbeddingSegmentsSum},
        {"EmbeddingBagPackedSum", Type::EmbeddingBagPackedSum},
        {"EmbeddingBagOffsetsSum", Type::EmbeddingBagOffsetsSum},
        {"Gather", Type::Gather},
        {"GatherElements", Type::GatherElements},
        {"GatherND", Type::GatherND},
        {"GridSample", Type::GridSample},
        {"OneHot", Type::OneHot},
        {"RegionYolo", Type::RegionYolo},
        {"ShuffleChannels", Type::ShuffleChannels},
        {"DFT", Type::DFT},
        {"IDFT", Type::DFT},
        {"RDFT", Type::RDFT},
        {"IRDFT", Type::RDFT},
        {"Abs", Type::Math},
        {"Acos", Type::Math},
        {"Acosh", Type::Math},  
        {"Asin", Type::Math},
        {"Asinh", Type::Math},
        {"Atan", Type::Math},
        {"Atanh", Type::Math},
        {"Ceil", Type::Math},
        {"Ceiling", Type::Math},
        {"Cos", Type::Math},
        {"Cosh", Type::Math},
        {"Floor", Type::Math},
        {"HardSigmoid", Type::Math},
        {"If", Type::If},
        {"Neg", Type::Math},
        {"Reciprocal", Type::Math},
        {"Selu", Type::Math},
        {"Sign", Type::Math},
        {"Sin", Type::Math},
        {"Sinh", Type::Math},
        {"SoftPlus", Type::Math},
        {"Softsign", Type::Math},
        {"Tan", Type::Math},
        {"CTCLoss", Type::CTCLoss},
        {"Bucketize", Type::Bucketize},
        {"CTCGreedyDecoder", Type::CTCGreedyDecoder},
        {"CTCGreedyDecoderSeqLen", Type::CTCGreedyDecoderSeqLen},
        {"CumSum", Type::CumSum},
        {"DetectionOutput", Type::DetectionOutput},
        {"ExperimentalDetectronDetectionOutput",
                      Type::ExperimentalDetectronDetectionOutput},
        {"LogSoftmax", Type::LogSoftmax},
        {"TopK", Type::TopK},
        {"GatherTree", Type::GatherTree},
        {"GRN", Type::GRN},
        {"Range", Type::Range},
        {"Proposal", Type::Proposal},
        {"ReorgYolo", Type::ReorgYolo},
        {"ReverseSequence", Type::ReverseSequence},
        {"ExperimentalDetectronTopKROIs", 
                      Type::ExperimentalDetectronTopKROIs},
        {"ExperimentalDetectronROIFeatureExtractor",
                      Type::ExperimentalDetectronROIFeatureExtractor},
        {"ExperimentalDetectronPriorGridGenerator",
                      Type::ExperimentalDetectronPriorGridGenerator},
        {"ExperimentalDetectronGenerateProposalsSingleImage",
                      Type::ExperimentalDetectronGenerateProposalsSingleImage},
        {"ExtractImagePatches", Type::ExtractImagePatches},
        {"GenerateProposals", Type::GenerateProposals},
        {"Inverse", Type::Inverse},
        {"NonMaxSuppression", Type::NonMaxSuppression},
        {"NonMaxSuppressionIEInternal", Type::NonMaxSuppression},
        {"NMSRotated", Type::NonMaxSuppression},
        {"MatrixNms", Type::MatrixNms},
        {"MulticlassNms", Type::MulticlassNms},
        {"MulticlassNmsIEInternal", Type::MulticlassNms},
        {"Multinomial", Type::Multinomial},
        {"Reference", Type::Reference},
        {"Subgraph", Type::Subgraph},
        {"PriorBox", Type::PriorBox},
        {"PriorBoxClustered", Type::PriorBoxClustered},
        {"Interaction", Type::Interaction},
        {"MHA", Type::MHA},
        {"Unique", Type::Unique},
        {"Ngram", Type::Ngram},
        {"ScaledDotProductAttention", Type::ScaledDotProductAttention},
        {"ScaledDotProductAttentionWithKVCache", 
                          Type::ScaledDotProductAttention},
        {"PagedAttentionExtension", Type::ScaledDotProductAttention},
        {"RoPE", Type::RoPE},
        {"GatherCompressed", Type::Gather},
        {"CausalMaskPreprocess", Type::CausalMaskPreprocess},
    };
    return type_to_name_tbl;
}

Забавно, что многие, увидев приведённый выше код, сочтут выполнение задачи по поиску ошибок в нём неразумной потерей времени. Как результат, сразу пролистают сюда, дабы посмотреть ответ.

Самое забавное, что, понимая всю неэффективность и учитывая все трудозатраты, большинство программистов продолжают вручную искать ошибки вместо того, чтобы использовать технологию статического анализа.

Казалось бы, что может быть проще: запускаем анализ и после смотрим на предупреждения с уже готовым списком ошибок. Не надо тратить время на поиск проблемных мест — они уже подсвечены. Остаётся только исправлять.

Предупреждения анализатора:

  1. V766 An item with the same key '«Abs»' has already been added. cpu_types.cpp 178

  2. V766 An item with the same key '«SoftPlus»' has already been added. cpu_types.cpp 198

Если кратко и по делу, то вот они — опечатки:

static const TypeToNameMap& get_type_to_name_tbl() {
  static const TypeToNameMap type_to_name_tbl = {
    ....,
    {"Abs", Type::Eltwise},                   // <=
    ....,
    {"SoftPlus", Type::Eltwise},              // <=
    ....,
    {"Abs", Type::Math},                      // <=
    ....,
    {"SoftPlus", Type::Math},                 // <=
    ...., 
  };
  return type_to_name_tbl;
}

Очевидно, что одинаковых значений быть не должно, и дубликаты никогда не будут использованы.

Заключение

Вот такое вот интересное приключение по опечаткам у нас получилось.

Это была первая часть статьи про проверку кода проекта OpenVINO. Желаю вам быть более аккуратными, и да прибудет с вами сила концентрации внимания.

Всю информацию мы, конечно же, передадим разработчикам и будем надеяться, что баги в ближайшем будущем исправят.

Хочется ещё раз напомнить: найденные опечатки и связанные с ними ошибки говорят лишь о том, что программисты тоже люди. А для счастья, (и исправления ошибок в коде) им всего лишь нужен хотя и маленький, но эффективный и свой — статический анализатор кода.

И, как у нас уже исторически сложилось, предлагаю попробовать наш анализатор PVS-Studio. Для Open Source проектов у нас предоставляется бесплатная лицензия.

Берегите себя и всего доброго!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Gorshkov. 12 moments of typos and copy-paste, or why AI hallucinates: checking OpenVINO.

Habrahabr.ru прочитано 2168 раз