Векторизация функции с использованием преимуществ параллелизма

Для простой нейронной сети я хочу применить функцию ко всем значениям гонума VecDense,

Гонум имеет Apply Метод для плотных матриц, но не для векторов, поэтому я делаю это вручную:

func sigmoid(z float64) float64 {                                           
    return 1.0 / (1.0 + math.Exp(-z))
}

func vSigmoid(zs *mat.VecDense) {
    for i := 0; i < zs.Len(); i++ {
        zs.SetVec(i, sigmoid(zs.AtVec(i)))
    }
}

Кажется, это очевидная цель для одновременного выполнения, поэтому я попытался

var wg sync.WaitGroup

func sigmoid(z float64) float64 {                                           
    wg.Done()
    return 1.0 / (1.0 + math.Exp(-z))
}

func vSigmoid(zs *mat.VecDense) {
    for i := 0; i < zs.Len(); i++ {
        wg.Add(1)
        go zs.SetVec(i, sigmoid(zs.AtVec(i)))
    }
    wg.Wait()
}

Это не работает, возможно, не неожиданно, так как Sigmoid() не заканчивается wg.Done(), поскольку оператор возврата (который выполняет всю работу) идет после него.

Мой вопрос: как я могу использовать параллелизм, чтобы применить функцию к каждому элементу вектора гонума?

1 ответ

Решение

Прежде всего обратите внимание, что эта попытка сделать вычисления одновременно предполагает, что SetVec() а также AtVec() методы безопасны для одновременного использования с разными индексами. Если это не так, то попытка решения по своей сути небезопасна и может привести к скачкам данных и неопределенному поведению.


wg.Done() следует вызвать сигнал о том, что "рабочая" рутина закончила свою работу. Но только когда горутин закончил свою работу.

В вашем случае это не (только) sigmoid() функция, которая запускается в рабочей программе, а точнее zs.SetVec(), Так что вы должны позвонить wg.Done() когда zs.SetVec() вернулся, не раньше.

Одним из способов было бы добавить wg.Done() до конца SetVec() метод (это также может быть defer wg.Done() в начале), но было бы невозможно ввести эту зависимость (SetVec() не должен знать о каких-либо группах ожидания и goroutines, это серьезно ограничило бы его удобство использования).

Самый простой и чистый способ в этом случае - запустить анонимную функцию (литерал функции) в качестве рабочей программы, в которой вы можете вызывать zs.SetVec()и в котором вы можете позвонить wg.Defer() как только вышеупомянутая функция вернулась.

Что-то вроде этого:

for i := 0; i < zs.Len(); i++ {
    wg.Add(1)
    go func() {
        zs.SetVec(i, sigmoid(zs.AtVec(i)))
        wg.Done()
    }()
}
wg.Wait()

Но это само по себе не сработает, так как литерал функции (closure) ссылается на переменную цикла, которая изменяется одновременно, поэтому литерал функции должен работать со своей собственной копией, например:

for i := 0; i < zs.Len(); i++ {
    wg.Add(1)
    go func(i int) {
        zs.SetVec(i, sigmoid(zs.AtVec(i)))
        wg.Done()
    }(i)
}
wg.Wait()

Также обратите внимание, что у подпрограмм (хотя они могут быть легкими) есть накладные расходы. Если работа, которую они выполняют, "мала", накладные расходы могут перевесить выигрыш в производительности при использовании нескольких ядер / потоков, и в целом вы не сможете повысить производительность, выполняя такие небольшие задачи одновременно (черт, вы можете даже сделать хуже, чем без использования подпрограмм), Мера.

Также вы используете мини-программы для выполнения минимальной работы, вы можете улучшить производительность, не "выбрасывая" программы после того, как они выполнят свою "крошечную" работу, но вы можете их "использовать" повторно. См. Связанный вопрос: это пул рабочих потоков идиоматического в Go?

Другие вопросы по тегам