Анализ слоя IR в рамках ИИ

искусственный интеллект

Аннотация: Эта статья посвящена анализу особых потребностей структуры AI для ИК и отрасли, и некоторые мысли от MindSpore.

Эта статья опубликована в сообществе HUAWEI CLOUD.《Колонка технологий MindSpore | Анализ уровня IR в AI Framework》, автор оригинала: полная жизненных сил девушка месяц.

IR (промежуточное представление) является посредником перевода между исходным кодом и целевым кодом в процессе компиляции программы Дизайн IR очень важен для компилятора Хороший IR должен учитывать полноту компиляции из исходного кода в целевой код , производительность, простота использования оптимизации компиляции и производительность. А в чем суть ИИ-фреймворка? Суть ИИ-фреймворка состоит в том, чтобы перевести выражение модели пользователя в исполняемый код, а затем выполнить эффективное выполнение (обучение и вывод).От выражения модели пользователя (например, глубокой нейронной сети) до конечного исполняемого кода Поведение компилятора, у этого компилятора тоже есть IR, и его дизайн играет решающую роль в полноте/гибкости/простоте использования/производительности ИИ-фреймворка.

В этой статье основное внимание уделяется анализу особых требований к ИИ-фреймворку для IR, решениям, которые есть в отрасли, и некоторым мыслям о MindSpore. Прежде всего, я познакомлю вас с классификацией общих IR компиляторов и их соответствующими характеристиками.

Введение в индустрию IR

1. По своей организационной структуре [1] ИР можно разделить на: Линейные ИР (Linear IR), Графические ИР (Graphical IR), Гибридные IR (Hybrid IR), среди которых

  • Линейный ИК:

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

  • Гибридный ИК:

Сочетает в себе элементы графового IR и линейного IR. Обычный гибридный IR использует линейный IR низкого уровня для представления блоков кода без петель и граф IR для представления потока управления между этими блоками.

  • Графический ИК:

Сохраните знания/информацию о процессе компиляции в графе, и соответствующий алгоритм будет описан путем работы с объектами (узлами, ребрами, списками и деревьями) в графе.

Примером линейного IR является Stack-Machine Code, представляющий собой одноадресный код, предполагающий, что операнды хранятся в стеке. Большинство операций берут свои операнды из стека и помещают свои результаты в стек. Например: машинный код стека, соответствующий выражению b-a*3, выглядит следующим образом:

push 3
push a
multiply
push a
substract

LLVM IR — типичный гибридный IR, который содержит два уровня (CFG+BB):

Верхний уровень — это график потока управления (сокращенно CFG), который представляет поток управления между базовыми блоками (BB). Каждый узел (Node) CFG является базовым блоком, и существует ребро (Edge) между базовыми блоками b1 и b2: b1->b2, если поток управления может протекать от последней инструкции базового блока b1 к первая инструкция базового блока b2 инструкция

Нижний слой — базовый блок, в базовом блоке каждая инструкция представлена ​​в виде SSA (Static Single Assignment), и эти инструкции образуют линейный список инструкций

IR Sea of ​​Nodes (автор Cliff Click) — типичный граф IR [2], в котором упрощена двухслойная структура инструкции BB+SSA в графе CFG, удален BB, а остальное содержит только инструкции однослойная структура. Вводя специальные инструкции REGION, IF, PROJECTION, он ослабляет инструкции общего порядка в блоке BB до явных зависимостей данных и зависимостей управления и использует те же методы представления и обработки для зависимостей управления и зависимостей данных, так что упрощенный анализ и преобразование IR . Ниже приведен простой пример IR:

В этом примере прямоугольники — это узлы графа, представляющие инструкции SSA, а стрелки — ребра графа, сплошные стрелки — зависимости элементов управления, открытые стрелки — зависимости данных. Как видно из этого примера, зависимости use-def явно включены в этот IR, и никаких дополнительных вычислений не требуется.

Основываясь на явной информации use-def в этом IR, можно легко реализовать два типа оптимизации: оптимизация времени синтаксического анализа (пессимистическая) и глобальная оптимизация (оптимистичная).

Во время Parse, поскольку нет всей информации о программе, можно сделать только частичную оптимизацию, такую ​​как оптимизация глазка (например: сворачивание констант, Identity-функция). Разработав подходящую иерархию классов и наследования, можно добиться оптимизации с помощью простого алгоритма:

Для глобальной оптимизации, такой как Sparse Conditional Constant Propagation (SCCP), это также может быть реализовано очень просто: сначала цепочки def-use вычисляются на основе явного use-def в графе, а затем его можно легко реализовать. SCCPSea of ​​Nodes IR предоставляет очень важную идею: явно представлять информацию о зависимости в графе IR. Эта идея продолжается в FIRM IR
2. Анализируя IR с точки зрения распространенных языков программирования, мы видим, что форма IR делится на два разных лагеря: один представляет собой IR компилятора для императивных языков программирования, а другой — компилятор для языков функционального программирования. компилятор IR императивного языка программирования принимает SSA в качестве базовой формы, и я не буду повторять это здесь.Далее основное внимание уделяется IR функционального языка программирования.В IR функционального языка программирования CPS или ANF является его базовой композицией форма 1. Стиль с прохождением продолжения (CPS) дословно переводится как: стиль с непрерывным прохождением CPS представляет собой такую ​​форму: функция f помимо собственных параметров всегда имеет лишний параметр, продолжение continue также является функцией, когда f завершается После вычисляя собственное возвращаемое значение, вместо того, чтобы возвращать его, используйте возвращаемое значение в качестве параметра продолжения и вызывайте продолжение. Поэтому функция в виде CPS не вернется с формальной точки зрения, а когда захочет вернуться, то передаст все параметры в продолжение, так что продолжение продолжит выполняться. Например:

def foo(x):
return x+1

Преобразованный в форму CPS, k является продолжением:

def foo(x,k):
k(x+1)

Интуитивно функции не "возвращают", а "продолжают". Преимущество CPS состоит в том, чтобы сделать явной следующую информацию: возврат процедуры (вызов продолжения), промежуточное значение (с явным именем), порядок вычисления, хвостовой вызов ( вызовите процедуру с тем же продолжением), например следующий фрагмент кода Python, чтобы найти произведение всех простых чисел, меньших n.

def prodprimes(n):
    if n == 1:
        return 1
    if isprime(n):
        return n * prodprimes(n - 1)
return prodprimes(n - 1)

При выражении в форме CPS:

def prodprimes(n, c):
    def k(b):
        if b == True:
            m = n - 1
            def j(p):
                a = n * p
                c(a)
            prodprimes(m, j)
        else:
            def h(q):
                c(q)
            i = n - 1
            prodprimes(i, h)
    if n == 1:
        c(1)
    else:
        isprime(n, k)

Как видно из приведенного выше кода, «возврат процедуры» заменяется вызовом продолжений, таких как c, j, k, h; промежуточные значения a, b, m, i — все заданные имена переменных. Форма CPS очень подходит для анализа и преобразования компилятора, такого как преобразование исключения хвостовой рекурсии: если конец функции f должен вызвать функцию g, то продолжение функции g не обязательно должно быть продолжением, сгенерированным в f, но можно заменить на Продолжение, переданное f. В исходном коде приведенного выше примера оператор «return prodprimes(n - 1)» представляет собой хвостовую рекурсию в форме CPS. Можно ясно видеть, что определение h(q) фактически равно c(q), поэтому можно сказать, что h равно c, поэтому можно выполнить следующее преобразование [3]:

def h(q):                         i = n - 1
    c(q)            ->           prodprimes(i, c)
i = n - 1
prodprimes(i, h)

Хотя CPS очень последовательный и мощный, большая проблема заключается в том, что его трудно читать. Итак, существует форма A-norm Form (ANF) форма 2. Форма ANF напрямую преобразует исходный код Direct Style [4], минуя форму CPS.

Форма ANF делит выражения на две категории: атомарные выражения и составные выражения.

Атомарное выражение представляет постоянное значение, переменную, примитивную или анонимную функцию. Составное выражение состоит из нескольких атомарных выражений, которые можно рассматривать как анонимную функцию или вызов примитивной функции. Первым входом комбинации является функция быть вызвана, остальные входные данные являются аргументами вызова. Составное выражение либо пусть-связано с переменной, либо оно может появиться только в последней позиции.Как вы можете видеть, форма ANF явно выражает промежуточные значения и поток управления и порядок оценки через let-bound.Его грамматика определяется следующим образом [5]

<aexp> ::= NUMBER | STRING | VAR | BOOLEAN | PRIMOP
          |  (lambda (VAR …) <exp>)
<cexp> ::= (<aexp> <aexp> …)
          |  (if <aexp> <exp> <exp>)
<exp> ::= (let ([VAR <cexp>]) <exp>) | <cexp> | <aexp>

Например, приведенная выше функция prodprimes, если она выражена в приведенной выше грамматике, должна быть:

(define prodprimes
  (lambda (n)
    (let (a (= n 1))
      (if a 1 (let (b isprime(n))
                   (if b (let (m (- n 1))
                           (let (p (prodprimes m))
                             (* n p)))
                         (let (s (- n 1))
                           (prodprimes m))
                    ))))))

Это выражение формы ANF, если его перевести на python, должно быть похоже на:

def prodprimes(n):
    r = n == 1
    if r:
        return 1
    b = isprime(n)
    if b:
        m = n - 1
        p = prodprimes(m)
        return n * p
    s = n - 1
return prodprimes(s)

Из этого кода также видно, что форма ANF проще и понятнее, чем форма CPS.

Роль слоя IR в структуре ИИ

Теперь все основные фреймворки ИИ имеют многоуровневый IR.Хороший IR-уровень способствует компиляции, оптимизации и выполнению моделей ИИ и является основой для эффективного обучения и вывода фреймворков ИИ.С точки зрения обучения в настоящее время существует три типа фреймворков ИИ в отрасли. Режимы: режим выполнения Eager, режим выполнения графа и режим выполнения Staging (смешанный), в котором высокопроизводительный режим (режим выполнения Graph и режим выполнения Staging) основан на уровне IR: режим выполнения Eager обычно использует функции основного языка (сейчас в основном Python) интерпретируются и выполняются, и в нем используются некоторые методы перегрузки и Tape.

Режим выполнения Graph в основном предназначен для получения структуры графа модели AI, а затем компиляции, оптимизации и выполнения.Компиляция, оптимизация и выполнение здесь основаны на IR графа.Теперь есть три способа получить структуру графа. модели ИИ: первый — программист.Используйте состав API (версия TF1.x и т. д.) Второй — Tracing JIT (тенденция, привнесенная JAX, теперь поддерживаемая TF2.0/Pytorch и т. д.), то есть запустить симуляцию сценария модели пользователя и получить последовательность прямого выполнения, а затем выполнить композицию на основе этой последовательности.Преимущество заключается в том, что его легче сопоставить с режимом Eagle.Недостаток простой реализации заключается в том, что преобразование управления поток является более проблематичным.Если последовательность выполнения связана с результатом выполнения оператора, это непросто реализовать, и нелегко справиться с побочными эффектами.Поэтому AutoGraph TF также необходимо объединить анализ AST для решить проблему преобразования потока управления.Третий - AST JIT (TorchScript от Pytorch) на основе AST Python для композиции.Преимущество заключается в том, что функции преобразования могут быть более полными, включая поток управления и т. д. Для реализации функций требуется много работы
Режим выполнения Staging аналогичен режиму Eager, через модификатор Python некоторые подграфы компилируются и выполняются для ускорения (с помощью Tracing JIT или AST JIT), также используется граф IR.

С точки зрения логического вывода, когда ИИ-фреймворк генерирует окончательную модель логического вывода, требуется много оптимизаций компиляции, таких как квантование, обрезка и т. д., которые обычно выполняются на уровне IR, а окончательная Формат модели логического вывода также прямо или косвенно используется в графе Layer IR AI Framework Layer IR Требования и проблемы По сравнению с другими IR общего назначения, уровень IR фреймворка AI имеет некоторые особые требования и проблемы:

**Тензорное выражение: **модель AI в основном имеет дело с тензорными данными, что сильно отличается от обычных приложений, но добавление тензорных типов данных не составляет труда для IR компилятора.

**Автоматическая дифференциация: **Дифференциация - это самое большое различие между разработкой модели ИИ и общей разработкой приложений. Современные платформы ИИ будут обеспечивать функции автоматической дифференциации. Проблема заключается в простоте реализации, производительности и масштабируемости будущей дифференциации высокого порядка.

**Возможность JIT:** Будь то режим графика или промежуточный режим, с точки зрения инженера-алгоритма, поскольку этапы компиляции не отображаются, его можно рассматривать как режим JIT. Производительность компиляции является серьезной проблемой для JIT

**Неявный параллелизм: **С точки зрения разработчика существует два способа параллелизма. Один из них — явный параллелизм.

**Модификатор Parallel: **Существует также неявный параллельный метод, который анализирует зависимости через компилятор и автоматически реализует параллелизм.Вообще говоря, традиционный компилятор CFG+BB использует анализ полного порядка для анализа программы, что удобно делать. параллелизм; функциональные компиляторы теоретически легко анализируют зависимости данных и облегчают неявную параллельную оптимизацию. Интересно, что в сценариях глубокого обучения на выполнение ядра приходится большая часть накладных расходов, а реализация асинхронного параллелизма во время выполнения также может значительно повысить общую производительность Роль неявного параллелизма будет относительно ослаблена, но для достижения максимальной производительности неявный параллелизм все еще работает.

**Оптимизация цикла:** расчет ИИ включает в себя большое количество тензорных операций, то есть оптимизацию цикла (тензор -> скаляр -> векторизация) для компилятора, но эта проблема в основном связана с IR уровня оператора. Конечно, Layer IR также является IR компилятора, который должен быть универсальным, включая базовые функции, такие как система типов, поток управления и анализ потока данных, а также устранение побочных эффектов.

Некоторые жанры в индустрии на слое IR

**Преобразование вычислительного графа:** Реализация, ориентированная на DAG. Многие ранние платформы использовали эту схему для вычисления НП графа. Структура НП относительно естественна. Вычислительный граф в основном состоит из ребер и узлов, а узлы обычно используются для выражения операторов, переменных, констант и т. д. Ребра соответствуют тензорам, которые фактически выражают зависимость данных. Следующие автоматические дифференцирование и оптимизация основаны на этом DAG.Преимущества этой схемы в том, что она проста и интуитивно понятна, а потери производительности при оптимизации невелики.Недостатком является то, что расчетный граф IR не является реальным формальным компилятором IR. Неполная поддержка (например, рекурсии), обработки побочных эффектов, потока управления и анализа потока данных.

**CFG+BB:** Layer IR основан на IR традиционных компиляторов, таких как TorchScript, Julia и т. д. Как добиться автоматического дифференцирования? В качестве примера возьмем Julia Zygote [6]: для обычного кода (не фи, не ветвления) внутри блока BB с помощью цепного правила можно генерировать код AD в обратном порядке

После выражения приведенного выше выражения в виде SSA, вставки J и вычисления AD можно получить псевдокод SSA, показанный на следующем рисунке:

%6 на приведенном выше рисунке называется «альфа-узел», который соответствует узлу %6 в Primal, то есть B3 в верхней строке, обратной функции операции «/».

Для потока управления между CFG необходимо обратить анализ потока управления и вставить соответствующие фиктивные фи-узлы в основную CFG для записи и воспроизведения потока управления. Например, этот фрагмент кода для расчета мощности:

В соответствующей исходной CFG фи-узел %1 вставляется как фиктивный фи-узел для записи потока управления. Затем используйте этот %1 в AD CFG для управления (%1 записывает поток управления через push, затем поток управления воспроизведением в AD CFG через pop)

Благодаря последующей оптимизации кода Power-код AD похож на следующий псевдокод:

Видно, что автоматическая дифференциация CFG+BB, наконец, реализована путем итерации.Форма SSA с Scope должна решить проблему переноса границ, что принесет некоторые проблемы с обработкой для автоматической дифференциации.

**Как сделать оптимизацию графа? ** Преобразовать в форму use-def, def-use для оптимизации

**Как сделать параллельную оптимизацию? **Поскольку CFG+BB является методом полного заказа, его необходимо преобразовать в определение использования и проанализировать в сочетании с информацией о побочных эффектах.

Преимуществами использования схемы CFG+BB являются полные функции, зрелые схемы и высокая возможность повторного использования.Однако форма CFG+BB требует определенной работы по преобразованию для автоматического дифференцирования/оптимизации графа/параллельной оптимизации, что не так интуитивно понятно и эффективно.

функциональный ИК

Используйте функциональную IR для выполнения IR уровня, например, Relay, Myia и т. д., как добиться автоматической дифференциации? Для неуправляющего потока метод расчета AD такой же, как и метод расчета AD в блоке BB, описанном выше. Для потока управления функциональный IR использует другой подход, преобразуя итерацию в рекурсию и выбор ветвления с помощью функции переключения. Например, та же функция pow() выше:

def pow(x, n):
    return header_pow(n, 1, x)
def header_pow(phi_n, phi_r, x):
def body_pow():
    phi_n_1 = phi_n - 1
    phi_r_1 = phi_r * x
        return header_pow(phi_n_1, phi_r_1, x)
    def after_pow():
        return phi_r
    f = switch(phi_n > 0, header_pow, after_pow)
    f()

Возьмем pow(5,3) в качестве примера, процесс рекурсивного вызова выглядит следующим образом:

pow(5, 3) -> header_pow(3, 1, 5) -> body_pow() -> header_pow(2, 5, 5) -> body_pow() -> header_pow(1, 5*5, 5) -> body_pow -> header_pow(0, 5*5*5, 5) -> after_pow() (на этот раз вернуть 5*5*5)

Можно видеть, что вызов и возврат рекурсивного вызова здесь соответствуют вышеупомянутым операциям push и pop phi-узла потока управления CFG+BB соответственно.

Поскольку процесс AD представляет собой процесс преобразования функции, граф после AD также является структурой рекурсивного вызова, поэтому нет необходимости в операциях стекирования и выталкивания phi-узлов потока управления, подобных CFG+BB, и рекурсивном процессе вызова, естественно, заменяет ввод Процесс укладки и извлечения

# Производная по x

def x_grad_pow(x, n):
    phi_n = n
    phi_r = 1
    return x_bprop_header_pow(phi_n, phi_r, x, 1)

def x_bprop_header_pow(phi_n, phi_r, x, sens):
    def env_x_bprop_body_pow():
        %3 = x_bprop_header_pow(phi_n – 1, phi_r * phi_x, x, 1)
        %4 = phi_r_bprop_header_pow(phi_n – 1, phi_r * phi_x, x, 1)
        %5 = %4 * phi_r
        return %3 + %5
    def env_x_bprop_after_pow():
        return 0

    f = switch(phi_n > 0, env_x_bprop_body_pow, env_x_bprop_after_pow)
    r = switch(phi_n > 0, f(), 0)
    return r

def phi_r_bprop_header_pow(phi_n, phi_r, x, sens):
    def env_phi_r_bprop_body_pow():
        %3 = phi_r_bprop_header_pow(phi_n - 1, phi_r * x, x, 1)
        %4 = %3 * x
        return %4

    def env_phi_r_bprop_after_pow():
        return 1

    if phi_n > 0:
        %5 = env_phi_r_bprop_body_pow()
    else:
        %5 = env_phi_r_bprop_after_pow()
return %5

Преимущество функциональной ИР состоит в том, что она дружественна к автоматическому дифференцированию и больше подходит для параллельного анализа, но проблема заключается в устранении побочных эффектов функциональной ИР и производительности функциональной ИР в состоянии выполнения (включая рекурсию, не дружественную к исполнению)

Дизайн-мышление в Mindspore

Слой IR MindSpore называется MindIR, Технический маршрут, выбранный MindIR, заключается в использовании функционального графа IR (имеется в виду Sea of ​​Nodes, Thorin, Myia и т. д.), который имеет следующие характеристики:

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

Основанный на графике больше подходит для возможности быстрой оптимизации JIT: он использует одноуровневое представление, подобное Sea of ​​Nodes IR, а поток управления и поток данных интегрированы, что больше подходит для JIT-оптимизации.

Форма ANF: Подобно Thorin, оба используют Graph IR и оба исключают Scope. Однако он использует не форму CPS Thorin IR, а форму ANF с аналогичными выразительными возможностями, более интуитивную и простую для проверки.MindIR надеется реализовать автоматическое дифференцирование и неявный параллельный анализ более удобно с помощью функционального метода.Метод на основе графа объединяет поток управления и поток данных.Унификация поддерживает более эффективную оптимизацию JIT. 1. Подробное объяснение MindIR [7] Грамматика MindIR унаследована от ANF, и ее определение выглядит следующим образом:

<ANode> ::= <ValueNode> | <ParameterNode>
<ParameterNode> ::= Parameter
<ValueNode> ::= Scalar | Named | Tensor | Type | Shape
               | Primitive | MetaFuncGraph | FuncGraph
<CNode> ::= (<AnfNode> …)
<AnfNode> ::= <CNode> | <ANode>

ANode в MindIR соответствует атомарному выражению ANF. ANode имеет два подкласса, ValueNode и ParameterNode. ValueNode означает, что константный узел может содержать постоянное значение (скаляр, символ, тензор, тип, размерность и т. д.) или может быть примитивная функция (Primitive) или мета-функция (MetaFuncGraph) или обычная функция (FuncGraph), потому что в функциональном программировании само определение функции также является значением, а ParameterNode — это узел параметра, представляющий формальный параметр функции. В MindIR CNode соответствует составному выражению ANF Формула указывает, что когда вызов функции автоматически дифференцируется в MindSpore, будет вычисляться вклад градиента ParameterNode и CNode, и будет возвращен градиент конечного ParameterNode, но градиент ValueNode не будет рассчитываться.

Давайте возьмем программу в качестве примера, чтобы сравнить и понять MindIR

def func(x, y):
 return x / y

@ms_function
def test_f(x, y):
    a = x - 1
    b = a + y
    c = b * func(a, b)
 return c

ANF, соответствующий этому коду Python, выражается как:

lambda (x, y)
    let a = x - 1 in
    let b = a + y in
    let func = lambda (x, y)
        let ret = x / y in
        ret end in
    let %1 = func(a, b) in
    let c = b * %1 in
    c end

Соответствующий MindIR:w.url.cn/s/Ansh1KW

В MindIR функциональный график (FuncGraph) представляет собой определение обычной функции. Функциональный график обычно состоит из ParameterNode, ValueNode и CNode, чтобы сформировать направленный ациклический граф, который может четко отображать процесс вычисления от параметров до возвращаемых значений. На рисунке выше видно, что две функции test_f и func в коде Python преобразуются в два графа функций, параметры которых x и y преобразуются в ParameterNodes графа функции, а каждое выражение преобразуется в CNode. Первый вход CNode связан с вызываемой функцией, такой как add, func и return на рисунке.Стоит отметить, что все эти узлы являются ValueNodes, потому что они понимаются как постоянные значения функции. Другие входы в CNode связывают параметры вызова, а значения параметров могут поступать из ParameterNode, ValueNode и других CNode.

В ANF каждое выражение связывается как переменная с выражением let, а зависимость от вывода выражения выражается ссылкой на переменную, тогда как в MindIR каждое выражение связывается как узел, через узел и Directed. ребра между узлами представляют зависимости

функциональная семантика

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

Функции высшего порядка

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

@ms_function
def hof(x):
 def f(x):
 return x + 3
 def g(function, x):
 return function(x) * function(x)
    res = g(f, x)
 return res

Соответствующий MindIR:w.url.cn/s/A8vb8X3

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

Поток управления выражается в MindIR в виде вызовов выбора функций более высокого порядка. Эта форма преобразует поток управления в поток данных функций более высокого порядка, тем самым делая алгоритмы автоматического дифференцирования более мощными. Он может поддерживать не только автоматическую дифференциацию потока данных, но и автоматическую дифференциацию потока управления, например условный переход, цикл и рекурсию. Ниже приведен простой пример использования Фибоначчи для демонстрации

@ms_function
def fibonacci(n):
 if(n < 1):
 return 0
 elif(n == 1):
 return 1
 else:
 return fibonacci(n-1) + fibonacci(n-2)

Соответствующий MindIR:w.url.cn/s/AUiE9Mc

Где Фибоначчи — это график функции верхнего уровня, и на верхнем уровне есть два графика функций, выбранных для вызова переключателем ✓ Фибоначчи — это истинная ветвь первого, если ✗фибоначчи — ложная ветвь первого, если . ✓✗fibonacci, называемый в ✗fibonacci, является истинной ветвью elif, а ✗✗fibonacci — ложной ветвью elif.

Ключ к пониманию здесь заключается в том, что в MindIR условные переходы и рекурсия выражаются в форме потока управления более высокого порядка.Например, ✓фибоначчи и ✗фибоначчи передаются как параметры оператора переключателя, и переключатель выбирает, какая функция соответствует к условным параметрам.Поэтому в качестве возвращаемого значения switch обрабатывает входную функцию как обычное значение и выполняет операцию бинарного выбора, а не вызывает ее, а реальный вызов функции завершается на CNode сразу после switch.

Бесплатные переменные и замыкания

Свободная переменная — это блок кода, который ссылается на переменную в среде с заданной областью действия, а не на локальную переменную.

Закрытие — это функция языка программирования, которая относится к комбинации блока кода и среды с заданной областью.

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

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

@ms_function
def func_outer(a, b):
 def func_inner(c):
 return a + b + c
 return func_inner

@ms_function
def ms_closure():
    closure = func_outer(1, 2)
    out1 = closure(1)
    out2 = closure(2)
 return out1, out2

Соответствующий MindIR:w.url.cn/s/AsUMXTS

В примере a и b являются свободными переменными, потому что переменные a и b в func_inner ссылаются на параметры, определенные в их родительском графе func_outer. Закрытие переменной — это замыкание, представляющее собой комбинацию функции func_inner и ее контекста func_outer(1, 2). Таким образом, результат out1 равен 4, потому что он эквивалентен 1+2+1, а результат out2 равен 5, потому что он эквивалентен 1+2+2.

использованная литература

[1] «Разработка компилятора», второе издание, глава 5. Промежуточное представление

[2] «Объединение анализов, объединение оптимизаций»

[3] Глава 1 "СОСТАВЛЕНИЕ ПРОДОЛЖЕНИЙ"

[4] «Функциональные языки программирования. Часть V: функциональные промежуточные представления»

[5]matt.might.net/articles

[6] «Не разворачивайте смежные: дифференциация программ SSA-Form»

[7] mindspore.cn/doc/note/z

Нажмите «Подписаться», чтобы впервые узнать о новых технологиях HUAWEI CLOUD~