Векторизация функции с использованием преимуществ параллелизма
Для простой нейронной сети я хочу применить функцию ко всем значениям гонума 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?