当前,稀疏matmult代码中包含以下行:
我假设这意味着我们想使用更通用的A_mul_B*!(α,A,B,β,C) = αAB + βC
方法来覆盖密集数组的C
(BLAS gemm
API)。 还是这样吗? (似乎也可以保留两个API,即保留A_mul_B*!(C,A,B)
方法,将其简单地定义为A_mul_B*!(C,A,B) = A_mul_B*!(1,A,B,0,C)
。)
我个人想为所有数组类型定义gemm
API(这已经在其他地方表达了)。 为密集数组实现这些方法似乎很简单,因为它们将直接调用gemm
等。 稀疏情况已经实现。 唯一真正的修改是对纯julia通用matmult ,它不接受α
和β
参数。
这将导致适用于任何类型的数组/数字的通用代码。 我目前有一个expm的简单实现(对_generic_matmatmult!
进行修改之后),它可以与bigfloats和稀疏数组一起使用。
参考 #9930,#20053,#23552。 最好!
感谢您的参考。 我想这个问题与添加gemm
-style方法而不是API修改有更多关系,但是如果我们认为它仍然与#9930过于相似,可以将其关闭。
首先,是否支持让_generic_matmatmul!
具有gemm
API? 这是一个相当简单的更改,并且纯粹是加法/不间断的,因为通过使用α=1
和β=0
可以简单地实现当前方法。 我可以做公关。 我可能会使用与此版本类似的
是。 那将是一个好的开始。 但是,我们需要考虑参数的顺序。 最初,我认为遵循BLAS排序更为自然,但我们相当先一致地具有输出参数,当前的三参数A_mul_B!
也是这种情况。 而且,正如您已经指出的那样,三参数版本将与具有α=1
和β=0
的五参数版本相对应,默认值参数位于最后。 当然,我们不必为此使用默认值语法,但是在这里使用它是有意义的。
为什么不仅仅引入通用的gemm
函数呢?
是。 那将是一个好的开始。 但是,我们需要考虑参数的顺序。 最初,我认为遵循BLAS排序更为自然,但我们首先要确保具有输出参数相当一致,当前的三参数A_mul_B!也是这种情况。 而且,正如您已经指出的那样,三参数版本将对应于α= 1和β= 0的五参数版本,并且默认值参数位于最后。 当然,我们不必为此使用默认值语法,但是在这里使用它是有意义的。
听起来不错。 我们可以在#9930中继续讨论实际的参数排序和方法重命名。 这更多的是仅提供五个参数的版本,因此我将保留当前的Ax_mul_Bx!(α,A,B,β,C)
接口。
为什么不仅仅引入通用的gemm函数呢?
除了上述更改,您是否建议将_generic_matmatmul!
重命名gemm!
?
更清楚地说,我确实认为我们应该最终使用单一方法mul(C,A,B,α=1,β=0)
,以及要分派的惰性转置/伴随类型,但这将是另一个PR。
为什么不仅仅引入通用的gemm函数呢?
我认为gemm
在Julia中是用词不当。 在BLAS中, ge
部分表示矩阵是通用的,即没有特殊结构,第一个m
是乘法,而列表m
是matrix 。 在Julia中, ge
(通用)部分和最后的m
(矩阵)一样被编码在签名中,因此我认为我们应该将其称为mul!
。
是SparseArrays.jl的符号mul!(α, A, B, β, C)
“最终化”为正式语法? 这是对mul!(C, A, B)
, lmul!(A, B)
和rmul!(A,B)
补充吗?
我不太喜欢$$$ A
, B
和C
的订单顺序。
SparseArrays.jl的符号
mul!(α, A, B, β, C)
“最终化”为正式语法?
我会说不。 最初,我喜欢遵循BLAS的想法(而且顺序也与通常的数学写法相匹配),但现在我认为仅将缩放参数添加为可选的第四和第五参数是有意义的。
因此,为了澄清一下,您需要某种意义上的可选参数
function mul!(C, A, B, α=1, β=0)
...
end
另一个选项是可选的关键字参数
function mul!(C, A, B; α=1, β=0)
...
end
但我不确定人们对Unicode是否太满意。
我对unicode感到非常满意,但是的确,我们尝试始终提供一个ascii选项,这在这里是可能的。 除非您知道BLAS,否则α
和β
名称也不是很直观,因此我认为在这里使用位置参数是更好的解决方案。
在我看来,更合乎逻辑的命名法是让muladd!(A, B, C, α=1, β=1)
映射到执行乘法和加法的各种BLAS例程。 (如上所述, gemm
,但是当A
是标量时,也可以是例如axpy
。)
然后,只要所有中间结果都可以存储在Y中,则mul!
函数可以有一个像mul!(Y, A, B, ...)
这样的接口,可以接受任意数量的参数(就像*
一样)。可选的kwarg可以指定乘法顺序,并具有合理的默认值)
mul!(Y, A::AbstractVecOrMat, B:AbstractVecOrMat, α::Number)
将具有默认实现muladd!(A, B, Y, α=α, β=0)
,两个矩阵/向量和一个标量的其他排列也将具有默认实现。
再次投票为密集矩阵定义了mul!(C, A, B, α, β)
。 这将允许编写用于密集和稀疏矩阵的通用代码。 我想在非线性最小二乘包中定义这样的函数,但我想这是盗版类型。
我也很想为MixedModels
包编写mul!(C, A, B, α, β)
方法并进行一些类型的盗版,但是如果这样的方法放在LinearAlgebra
会更好。包。 对于我来说,拥有用于此操作的muladd!
泛型的方法也很好。
我赞成,尽管我认为它的名称可能不只是mul!
。 muladd!
似乎是合理的,但肯定可以接受建议。
也许mulinc!
乘以增量?
也许我们可以像addmul!(C, A, B, α=1, β=1)
这样的东西?
这不是muladd!
吗? 还是将其称为addmul!
的想法是它会改变add参数而不是乘法参数? 有人会改变乘法参数吗?
请注意,在某些情况下,我们会对非第一元素进行突变,例如lmul!
和ldiv!
,因此我们可以按照通常的“ muladd”顺序(即muladd!(A,B,C)
)进行操作。 问题是α
和β
去哪个顺序? 一种选择是设置关键字参数?
如果您给实现者留下一个选项来分派标量α和β的类型,那不是很好吗? 为最终用户添加糖很容易。
我以为我们已经将mul!(C, A, B, α, β)
设置为α
, β
默认值。 我们在https://github.com/JuliaLang/julia/blob/b8ca1a499ff4044b9cb1ba3881d8c6fbb1f3c03b/stdlib/SparseArrays/src/linalg.jl#L32 -L50中使用此版本。 我认为有些软件包也在使用这种形式,但我不记得哪个在我头上。
谢谢! 如果记录下来,那将很好。
我以为我们已经将
mul!(C, A, B, α, β)
设置为α
,β
默认值。
SparseArrays使用它,但是我不记得在任何地方都在讨论它。
在某些方面, muladd!
名称更自然,因为它是一个乘法运算,后跟一个加法运算。 但是,α和β的默认值muladd!(C, A, B, α=1, β=0)
(请注意,β的默认值为0,而不是1),将其重新设置为mul!(C, A, B)
。
称它为mul!
还是muladd!
似乎是一种学究与一致性的问题,我认为SparseArrays中现有方法的情况会要求mul!
。 尽管我最喜欢奥斯卡·王尔德(Oscar Wilde)的话:“一致性是那些缺乏想象力的人的最后避难所”,但我发现自己一直在争论一致性。
在某些方面,
muladd!
名称更自然,因为它是一个乘法运算,后跟一个加法运算。 但是,α和β的默认值muladd!(C, A, B, α=1, β=0)
(请注意,β的默认值为0,而不是1),将其重新设置为mul!(C, A, B)
。
当C
包含Infs或NaNs时,会有一个有趣的例外:理论上,如果β==0
,结果仍应为NaNs。 实际上,这不会发生,因为BLAS和我们的稀疏矩阵代码会明确检查β==0
然后将其替换为零。
您可能会认为由于true
和false
默认值为α=true, β=false
分别为“ strong” 1和0,因此true*x
始终x
和false*x
始终zero(x)
。
lmul!
也应该具有这种特殊的行为: https :
true
和false
分别为“ strong” 1和0,这意味着true*x
始终x
和false*x
始终zero(x)
。
我不知道!:
julia> false*NaN
0.0
FWIW,我对该操作的LazyArrays.jl语法的可读性非常满意:
y .= α .* Mul(A,x) .+ β .* y
对于与BLAS兼容的阵列(带状和大跨度),在幕后它降低到mul!(y, A, x, α, β)
。
我不知道!
这是使im = Complex(false, true)
工作的一部分。
SparseArrays使用它,但是我不记得在任何地方都在讨论它。
上面在https://github.com/JuliaLang/julia/issues/23919#issuecomment -365463941中进行了讨论,并在https://github.com/JuliaLang/julia/pull/26117中实现,没有任何异议。 在稠密的情况下,我们没有α,β
版本,因此此回购中唯一可以立即生效的决定是SparseArrays
。
那么LinearAlgebra.BLAS.gemm!
呢? 它也不应该包装成5元mul!
吗?
它应该但没有人做过。 matmul.jl
有很多方法。
上面在#23919(注释)中进行了讨论,并在
好吧,这是我的反对意见。 我希望使用其他名称。
为什么要使用其他名称? 在密集和稀疏的情况下,基本算法都进行乘法和加法运算。
如果给这些函数起不同的名字,我们将有mul!(C,A,B) = dgemm(C,A,B,1,0)
和muladd!(C,A,B,α, β) = dgemm(C,A,B,α, β)
。
我看到的唯一好处是,如果我们实际拆分方法并在C = A*B
情况下保存if β==0
调用。
仅供参考,我开始在#29634中进行处理,以将接口添加到matmul.jl
。 我希望在确定名称和签名时完成它:)
muladd!
一个优点是,我们可以使用三元muladd!(A, B, C)
(或muladd!(C, A, B)
?)和默认的α = β = true
(如原始建议https中所述: //github.com/JuliaLang/julia/issues/23919#issuecomment-402953987)。 方法muladd!(A, B, C)
与Number
的muladd
类似,因此我想它是更自然的名称,尤其是如果您已经知道muladd
。
@andreasnoack似乎您先前的讨论是关于方法签名,并且比关键字参数更喜欢位置参数,而不是方法名称。 您是否对muladd!
这个名称有异议? (在SparseArrays
存在5元mul!
SparseArrays
可能是一个,但是定义向后兼容包装器并不难。)
当mul!
和muladd!
时,前者只是后者,其默认值为α
和β
似乎是多余的。 此外,BLAS已将add
部分规范化。 如果我们能为muladd!
提出一个可靠的通用线性代数应用程序,我想听听一下,但是否则我将避免冗余。
另外,我强烈希望我们将false
的强零属性与讨论mul!
分开。 IMO的β的任何零值都应该像在BLAS中以及在当前的五个自变量mul!
方法中一样强。 即,此行为应是mul!
而不是β
。 替代方案将难以使用。 例如mul!(Matrix{Float64}, Matrix{Float64}, Matrix{Float64}, 1.0, 0.0)
~~~不能使用BLAS。
我们无法更改BLAS的功能,但是_float _requiring_强零行为的确意味着每个实现都需要一个分支来检查零。
如果我们能为
muladd!
提出一个可靠的泛型线性代数应用
@andreasnoack通过这个,我想你的意思是“申请_three-argument_ muladd!
”,因为否则您将不同意包含5个参数的mul!
吗?
但是我仍然可以举一个例子,其中muladd!(A, B, C)
是有用的。 例如,如果要构建“小世界”网络,则对带状矩阵和稀疏矩阵进行“惰性”求和很有用。 然后,您可以编写如下内容:
A :: SparseMatrixCSC
B :: BandedMatrix
x :: Vector # input
y :: Vector # output
# Compute `y .= (A .+ B) * x` efficiently:
fill!(y, 0)
muladd!(x, A, y) # y .+= A * x
muladd!(x, B, y) # y .+= B * x
但是我不介意在那里手动编写true
,因为我可以将其包装起来以方便使用。 最重要的目标是将五参数函数作为稳定的文档化API。
回到重点:
当
mul!
和muladd!
时,前者只是后者,其默认值分别为α
和β
似乎是多余的。
但是,我们有一些*
来讲实现mul!
经适当初始化输出数组的“默认值”。 我认为Base
和标准库中可能有这样的“快捷方式”示例? 我认为同时拥有mul!
和muladd!
是有意义的,即使mul!
只是muladd!
的捷径。
我强烈希望我们将
false
的强零属性与讨论mul!
分开
我同意将重点放在讨论先由五参数形式的乘加运算( mul!
与muladd!
)的名称上是有建设性的。
当我问一个通用用例时,我做得不好,您需要muladd
来跨矩阵和数字进行通用工作。 数字版本为muladd
没有感叹号,所以我问的内容没有任何意义。
您的示例可以写成
mul!(y, A, x, 1, 1)
mul!(y, B, x, 1, 1)
所以我仍然看不到需要muladd!
。 仅仅是因为您认为这种情况太普遍了,以致于写1, 1
太冗长了?
但是,我们有一些
*
来讲实现mul!
与输出数组的“默认值”适当地初始化。 我认为Base
和标准库中可能有这样的“快捷方式”示例?
我没有这个。 您能不能详细说明? 您在这里谈论的捷径是什么?
所以我仍然看不到需要
muladd!
。 仅仅是因为您认为这种情况太普遍了,以致于写1, 1
太冗长了?
我认为muladd!
对其实际功能也更具描述性(尽管也许应该是addmul!
)。
我的名字muladd!
。 首先,我只是认为我们不必为此而工作,其次,我认为不赞成将mul!
推荐给muladd!
/ addmul!
是值得的。
仅仅是因为您认为这种情况太普遍了,所以写1、1太冗长了吗?
不。只要是公共API,我就可以调用五个参数函数。 我只是想举一个例子,我只需要三个参数版本(因为我认为那是您的要求)。
您在这里谈论的捷径是什么?
我认为这里定义的*
可以视为mul!
的快捷方式。 它是“只是”具有默认值的mul!
。 因此,为什么不让mul!
作为具有默认值的muladd!
/ addmul!
?
还有rmul!
和lmul!
定义为类似的“快捷方式”:
弃用
mul!
我以为讨论是关于添加新界面还是没有。 如果我们需要弃用mul!
来添加新的API,我认为这是不值得的。
我能想到的主要论据是:
addmul!(C, A, B)
而不是mul!(C,A,B,1,1)
或mul!(C,A,B,true,true)
。我认为这里定义的
*
可以视为mul!
的快捷方式。 它是“只是”具有默认值的mul!
。 所以,为什么不放手! 是具有默认值的muladd!
/addmul!
?
因为*
是矩阵相乘以及大多数用户将其相乘的默认方式。 相比之下, muladd!
使用率不会接近*
。 此外,它甚至是现有的运算符,而muladd!
/ addmul!
将是一个新函数。
不要以为rmul!
和lmul!
适合这种模式,因为它们通常不是默认的mul!
方法的默认值版本。
Simon在上面的帖子中很好地总结了这些好处。 问题是,好处是否足够大,足以证明重命名的额外功能(即弃用mul!
)。 这是我们不同意的地方。 我认为这不值得。
当您说重命名是不值得的时,您是否考虑到API不是完全公开的? 那样的话,我的意思是它不在Julia的文档中。
我知道LazyArrays.jl(和其他软件包?)已经在盲目使用,所以在semver之后就不好用了。 但是,它不像其他功能那样公开。
mul!
是从LinearAlgebra
导出并广泛使用的,因此我们现在绝对必须弃用它。 当A_mul_B!
变成mul!
或至少在0.7之前时,我们没有进行讨论是很可惜的,因为这是重命名函数的更好时机。
当我们可以分别更新stdlibs时,现在使用mul!
来更新LinearAlgebra v2.0
的名称怎么样?
LazyArrays.jl不使用mul!
因为它不适用于许多类型的矩阵(并在您使用StridedArray
覆盖时触发编译器慢度错误)。 它提供了形式的替代构造
y .= Mul(A, x)
我发现它更具描述性。 5个参数类似物是
y .= a .* Mul(A, x) .+ b .* y
我会赞成弃用mul!
并转移到LinearAlgebra.jl中的LazyArrays.jl方法,但这很难做到。
LowRankApprox.jl确实使用了mul!
,但是我可能会将其更改为使用LazyArrays.jl方法,从而避免了编译器错误。
好。 我以为只有两个建议。 但是显然有大约三个建议?:
mul!
muladd!
mul!
和五个参数muladd!
( muladd!
可称为addmul!
)
我以为我们在比较1和3。我现在的理解是@andreasnoack在比较1和2。
我会说2根本不是一个选项,因为三元参数mul!
是公共API并被广泛使用。 我所说的“ API并不完全公开”是指未记录五参数mul!
。
是的,我的计划是保留mul!
(作为3 arg,可能是4 arg的形式)。 我认为这是值得的,因为3 arg mul!
和addmul!
会有不同的行为,即给定addmul!(C, A, B, α, β)
,我们将:
mul!(C, A, B) = addmul!(C, A, B, 1, 0)
mul!(C, A, B, α) = addmul!(C, A, B, α, 0)
addmul!(C, A, B) = addmul!(C, A, B, 1, 1)
addmul!(C, A, B, α) = addmul!(C, A, B, α, 1)
但是,您可能不希望在实践中以这种方式实际实现它们,例如,将4-arg mul!
和addmul!
分别定义为5-arg addmul!
可能会更简单。
addmul!(C, A, B, α, β) = addmul!(C .= β .* C, A, B, α)
磕碰!
但是,您可能不希望在实践中以这种方式实际实现它们,例如,仅使用4-arg mul可能会更简单! 和addmul! 分别定义5-arg addmul! 如:
addmul!(C, A, B, α, β) = addmul!(C .= β .* C, A, B, α)
为什么不立即最佳地进行呢? 不这样做的目的在于,您只需要访问C
的元素一次,这对于大型矩阵肯定是更有效的。 另外,我很难相信仅定义5-arg addmul!
而不是分别定义4-arg mul!
和addmul!
会更长的代码。
仅供参考,我已经修改了LinearAlgebra的_generic_matmatmul!
实现以在LazyArrays中使用5个参数: https :
在这里通过以下方式调用:
materialize!(MulAdd(α, A, b, β, c))
但是实际代码(在tiled_blasmul!
)将很容易转换回LinearAlgebra。
可以采取什么措施来加快此过程? 我正在处理的事情将真正受益于具有就地mul + add的统一矩阵乘法API
Strided.jl的最新版本现在还支持5个参数mul!(C,A,B,α,β)
,在可能的情况下分派给BLAS,否则使用其自己的(多线程)实现。
@Jutho很棒的套餐! 有下一步的路线图吗? 计划最终与LinearAlgebra合并吗?
这绝不是我的意图,但是如果有人要求的话,我并不反对。 但是,我认为我在通用mapreduce
功能中自由使用@generated
函数(尽管只有一个)可能不太适合Base。
我的个人路线图:这主要是一个较低层的软件包,供较高层的软件包使用,即TensorOperations的新版本以及我正在开发的其他软件包。 但是,对基本线性代数的更多支持会很好(例如,将norm
应用于StridedView
当前会回落到Julia norm
相当慢的GPUArray
的mapreducekernel
实现同样普遍的
我认为到目前为止的共识是:
mul!(C, A, B)
C = αAB + βC
我建议首先关注5参数函数的名称,然后再讨论其他API(例如3参数和4参数addmul!
)。 但这是我们使用mul!
从_not_获得的“功能”,因此很难混用。
@andreasnoack您对@simonbyrne在https://github.com/JuliaLang/julia/issues/23919#issuecomment -431046516上面的评论解决了? 我认为没有必要弃用。
仅供参考,我刚刚完成了实施#29634。 感谢熟悉LinearAlgebra
可以查看它。
我认为将所有名称命名mul!
更简单,更好。 它还避免了弃用。 如果我们真的希望使用其他名称,则muladd
更好。
在讨论mul!
API时可能要考虑的其他事项:
当scale!
消失并陷入0.6-> 0.7的转变时,我感到有点难过,因为对我来说,标量乘法(向量空间的属性)与将对象自身相乘(代数的属性)非常不同)。 尽管如此,我已经完全接受了mul!
方法,并且非常感谢标量乘法不可交换时rmul!(vector,scalar)
和lmul!(scalar,vector)
的功能。 但是,现在我每天都对另外两个原位向量空间操作的非朱利安名称感到不安: axpy!
及其泛化axpby!
。 这些也可以吸收到mul!
/ muladd!
/ addmul!
。 尽管有点奇怪,但是如果A*B
中的两个因子之一已经是一个标量,则不需要额外的标量因子α
。
但是也许,类似于
mul!(C, A, B, α, β)
可能还有一个
add!(Y, X, α, β)
替换axpby!
。
@andreasnoack您对#23919上方的@simonbyrne的评论是否可以解决有关折旧/重新命名的问题(评论)? 我认为没有必要弃用。
请参阅https://github.com/JuliaLang/julia/issues/23919#issuecomment -430952179的最后一段。 我仍然认为引入新功能是不值得的。 无论如何,我认为我们应该弃用当前的5个参数mul!
。
@Jutho我认为将acp(b)y!
重命名add!
是个好主意。
参见#23919的最后一段
是的,我已经阅读并回复说,没有记录五参数mul!
,它也不是公共API的一部分。 因此,技术上不需要弃用。 请参阅https://github.com/JuliaLang/julia/issues/23919#issuecomment -430975159的最后一段(当然,无论如何都应该弃用,所以我已经在#29634中实现了它。)
在这里,我假设由于一个签名(例如mul!(C, A, B)
)的文档而导致的公共API声明不适用于其他签名(例如mul!(C, A, B, α, β)
)。 如果不是这种情况,我认为Julia和它的stdlib暴露了太多内部信息。 例如,这是Pkg.add
的书面签名
而实际的定义是
如果至少有一个Pkg.add
签名的文档的存在暗示其他签名是公共API,则Pkg.jl不会由于实现细节而删除该行为,而不会破坏主要版本,例如: Pkg.add(...; mode = :develop)
运行Pkg.develop(...)
; 支持Context!
所有关键字参数(可能实际上是预期的)。
但是无论如何,这只是我的印象。 您是否认为mul!(C, A, B, α, β)
与mul!(C, A, B)
一样公开?
我认为我们正在互相交谈。 我的意思是,我(仍然)认为引入其他功能并不值得。 因此,我参考了我以前的评论。 这与关于不赞成使用五参数mul!
的讨论是分开的。
但是,如果我们决定添加另一个函数,那么我认为最好弃用五个参数mul!
而不是仅仅破坏它。 当然,它不像三元参数mul!
那样常用,但是为什么不弃用它而不是仅仅破坏它呢?
这与关于不赞成使用五参数
mul!
的讨论是分开的。
我对您评论的最后一段的解释https://github.com/JuliaLang/julia/issues/23919#issuecomment -430952179是您承认@simonbyrne列出的好处https://github.com/JuliaLang/julia/issues / 23919#issuecomment -430809383用于新的五参数函数,但认为与保留_public API_(如您提到的“重新命名”和“弃用”)相比,它们的价值不高。 这就是为什么我认为考虑五个参数mul!
是否公开很重要的原因。
但是您还提到了拥有“额外功能”的理由,我想这就是您现在所指的。 您是否认为_C = AB_和_C =αAB+βC_的计算足够相似,因此相同的名称可以描述两者? 我实际上不同意,因为还有其他方法可以归纳三元参数mul!
:例如,为什么不为_y =A₁A²⋯A_ x_ mul!(y, A₁, A₂, ..., Aₙ, x)
https://github.com/JuliaLang/julia / issues / 23919#issuecomment -402953987?
为什么不弃用它而不是仅仅破坏它?
正如我在前面的评论中所说,我确实同意弃用五元参数mul!
是正确的做法,如果我们要引入另一个函数。 我的PR#29634中已经存在此代码。
您是否认为计算C = AB和C =αAB+βC足够相似,以至于相同的名称可以描述两者?
是的,因为前者只是带有β=0
的后者。 可以公平地说muladd!
/ addmul!
是C = αAB + βC
的更精确的名称,但是要到达那里要么需要引入另一个矩阵乘法函数( muladd!
/ addmul!
)或重命名mul!
,我认为现在不值得了。 如果在春季出现这种情况,那么考虑进行更改会更容易。
我实际上不同意,因为可以使用其他方法来概括三参数mul !:
朱莉娅碰巧定义了不带α
和β
参数的就地矩阵乘法方法,但是矩阵乘法传统实际上是基于BLAS-3,因此通用的矩阵乘法函数是C = αAB + βC
。
重命名
mul!
您是要在stdlib中还是在下游用户模块/代码中重命名它? 如果您是说前者,那么它已经在#29634中完成了(针对LinearAlgebra和SparseArrays),因此我认为您不必为此担心。 如果您指的是后者,我认为它又可以归结为公开或不公开的讨论。
矩阵乘法的传统确实基于BLAS-3
但是朱莉娅已经偏离了BLAS的命名约定。 因此,拥有更多描述性名称不是很好吗?
您是要在stdlib中还是在下游用户模块/代码中重命名它?
mul!
函数。 它添加了新功能addmul!
。但是朱莉娅已经偏离了BLAS的命名约定。
我说的不是名字。 至少不是完全正确,因为Fortran 77具有一些局限性,就函数名和分派而言我们都没有。 我说的是正在计算的东西。 BLAS-3中的通用矩阵乘法函数计算C = αAB + βC
而在Julia中, mul!
(fka A_mul_B!
)。
因此,拥有更多描述性名称不是很好吗?
会的,我已经说过几次了。 问题在于,我们应该拥有两个基本上可以完成相同任务的矩阵乘法函数,这并不是更好的选择。
29634不会重命名
mul!
函数。 它添加了新功能addmul!
。
我的意思是说,五个参数mul!
重命名为addmul!
。
问题在于,我们应该拥有两个基本上可以完成相同任务的矩阵乘法函数,这并不是更好的选择。
我觉得它们基本相同还是有些主观。 我认为_C =αAB+βC_和_Y =A₁A2⋯AₙX_都是_C = AB_的数学有效推论。 除非_C =αAB+βC_是唯一的概括,否则我认为论点不够充分。 这还取决于您是否了解BLAS API,并且我不确定这是否是典型Julia用户的基本知识。
同样,_C = AB_和_C =αAB+βC_在计算上非常不同,因为是否使用C
的内容。 它是前者的仅输出参数,而后者是输入-输出参数。 我认为这种差异值得视觉提示。 如果我看到mul!(some_func(...), ...)
和mul!
具有五参数形式,我必须计算参数的数量(当它们是函数调用的结果时很难,因为您必须匹配括号)看看some_func
是做一些计算还是分配。 如果我们有addmul!
那么我可以立即期望some_func
中的mul!(some_func(...), ...)
只分配。
我觉得它们基本相同还是有些主观。 我认为C =αAB+βC和Y =A₁A2⋯AₙX都是C = AB的数学有效推论。 除非C =αAB+βC是唯一的概括,否则我认为论点不够充分。
它可能不是唯一的概括,但是可以以大致相同的成本进行计算,并为构建其他线性代数算法提供了有用的原语。 在很多情况下,我想在实现各种与线性代数相关的算法时都拥有一个非零的beta,并且总是不得不降到BLAS.gemm!
。 如果没有中级临时人员,则无法一口气计算出您提到的其他概括,因此就地版本的用处不大。 此外,它们通常不如原始操作有用。
这还取决于您是否了解BLAS API,并且我不确定这是否是典型Julia用户的基本知识。
只要默认参数α=1
和β=0
仍然存在,则三个arg mul!
会执行任何没有BLAS背景的Julia用户所期望的事情。 对于更高级的选项,必须查阅手册,因为它可能与任何语言和任何功能有关。 此外,此单个mul!
调用不仅取代gemm
而且还取代gemv
和trmv
(奇怪的是,没有α
和BLAS API中的β
参数),可能还有许多其他参数。
我确实同意BLAS-3在计算方面是正确的概括,并且组合非常好。 我提出另一种可能的概括,只是因为我认为使用同一个名称进行辩解还不够“独特”。 另请参阅https://github.com/JuliaLang/julia/issues/23919#issuecomment -441267056的最后一段中的仅输出vs输入-输出参数。 我认为不同的名称使阅读/审阅代码更加容易。
此外,此单个
mul!
调用不仅取代gemm
而且还取代gemv
和trmv
(奇怪的是,没有α
BLAS API中的β
参数),可能还有很多其他参数。
是的,已经在#29634中实现,一旦确定名称(并获得审核)就可以开始使用!
在这次对话后,我遇到了一些困难(时间太长且无所事事...对不起!),领先的提案是否像mul!(C, A, B; α=true, β=false)
?
我不认为α和β的关键字参数在桌子上。 例如, @ andreasnoack在https://github.com/JuliaLang/julia/issues/23919#issuecomment -365762889中关闭了关键字参数。 @simonbyrne在https://github.com/JuliaLang/julia/issues/23919#issuecomment -426881998中提到了关键字参数,但他的最新建议https://github.com/JuliaLang/julia/issues/23919#issuecomment -431046516处于位置论点。
我们尚未确定名称(即mul!
vs addmul!
vs muladd!
),我认为这是中心主题(或者至少是我的愿望)。
您通常如何解决此类争议? 表决? 分流?
领先的提案是否像mul!(C,A,B;α= true,β= false)一样?
我喜欢这个,但是没有残酷。
没有看到关键字内容。 我也很犹豫添加unicode关键字。 我认为具有这些默认值的位置参数很好。 至于即将到来的投票,我的立场是简单的mul!
。 我认为这是mul!
的概括,它足够具体且有用,不需要新名称。
只是为了收集数据(至少现在是这样),让我们进行投票:
_C =αAB+βC_时您最喜欢的函数名称是什么?
mul!
addmul!
muladd!
对我来说addmul!
似乎描述了(A+B)C
而不是AB + C
。
起初,我对mul!
投了赞成票,然后看了一下操作,并认为“它先执行乘法运算,然后执行加法运算,显然我们应该将其称为muladd!
。 !
清楚地表明了它就位的事实,而缩放部分似乎适合关键字args。
它先做一个乘法,再做一个加法,显然我们应该称它为
muladd!
仅当您使用默认值β=true
,对于其他任何值,它再次变得更加通用。 那么,不叫它mul!
有什么意义呢,除了默认值β=false
还有其他任何值也可以给您带来更多的通用性吗? 与muladd(x,y,z) = x*y + z
相比,您如何排序参数? 会有些混乱,不是吗?
我认为muladd!
的缺点是听起来不是描述性的:描述性的命名类似于scalemuladd!
来提及缩放部分。
所以我更喜欢mul!
因为它的描述性不够强,不会导致期望。
就是说,我在LazyArrays.jl MulAdd
调用了惰性版本。
我更喜欢muladd!
而不是mul!
因为可以很好地将一个从未使用过C
( mul!
)值的函数与一个使用它的函数区分开( muladd!
)。
mul!
用于稀疏矩阵,对于密集的linalg也是一样的〜(不是新的论点)因此,我赞成将其称为mul!
。
- 从技术上讲,这是一个矩阵乘法:[AC] * [Bα; Iβ]
...如果eltype具有可换乘
...如果eltype是可交换的。
IIRC与@andreasnoack的讨论中,Julia只是将gemm
/ gemv
为y <- A * x * α + y * β
因为这很有意义。
@haampie这是个好消息! 当我在#29634中实现另一种方式时。
这是有限的帮助,但是
C = α*A*B + β*C
是表达操作的最好方法,因此,宏<strong i="8">@call</strong> C = α*A*B + β*C
或<strong i="10">@call_specialized</strong> ...
或类似的东西将是一个自然的界面-同样适用于类似情况。 然后可以将基础函数称为任何函数。
@dlfivefifty的@mschauer LazyArrays.jl具有出色的语法,可以像您的语法一样调用5个参数mul!
。
我认为我们需要首先建立基于函数的API,以便程序包作者可以开始针对其专用矩阵对其进行重载。 然后,Julia社区可以开始尝试糖的实验。
仅当您使用默认值
β=true
,对于其他任何值,它再次变得更加通用。 那么,不叫它mul!
,除了默认值β=false
,其他任何值也只会给您带来更一般的含义? 与muladd(x,y,z) = x*y + z
相比,您如何排序参数? 会有些混乱,不是吗?
当然,这里有一定的缩放比例,但是操作的“骨骼”显然是相乘和相加的。 我也可以使用muladd!(A, B, C, α=true, β=false)
来匹配muladd
的签名。 当然,必须记录在案,但这不言而喻。 这的确使我希望muladd
首先进入附加部分,但是船已经驶过了该部分。
与
muladd(x,y,z) = x*y + z
相比,您如何排序参数? 会有些混乱,不是吗?
这就是为什么我更喜欢addmul!
不是muladd!
。 我们可以确保参数的顺序与标量muladd
无关。 (尽管我更喜欢muladd!
胜过mul!
)
FWIW这里是到目前为止的论点的总结。 (我试图保持中立,但我赞成- muladd!
/ addmul!
所以请记住这一点...)
主要的分歧在于_C = AB_和_C =αAB+βC_是否足够不同以为后者命名。
它们足够相似是因为...
它是BLAS-3,可以很好地组合。 因此,_C =αAB+βC_是_C = AB_的明显推广(https://github.com/JuliaLang/julia/issues/23919#issuecomment-441246606,https://github.com/JuliaLang/julia/issues/ 23919#issuecomment-441312375等)
_“ muladd!
的缺点是听起来不是描述性的:描述性名称就像scalemuladd!
要提及缩放部分。” _ --- https://github.com/ JuliaLang /朱莉娅/问题/ 23919#issuecomment -441819470
_“从技术上讲,这是一个矩阵乘法:[AC] * [Bα;Iβ]” _ --- https://github.com/JuliaLang/julia/issues/23919#issuecomment -441825009
它们足够不同是因为...
_C =αAB+βC_大于乘法(https://github.com/JuliaLang/julia/issues/23919#issuecomment-430809383,https://github.com/JuliaLang/julia/issues/23919#issuecomment-427075792, https://github.com/JuliaLang/julia/issues/23919#issuecomment-441813176等)。
可能还有mul!
其他概括,例如Y = A₁ A₂ ⋯ Aₙ X
(https://github.com/JuliaLang/julia/issues/23919#issuecomment-402953987等)
仅输入与输入输出参数:具有一个基于参数数量使用C
的数据的函数会令人困惑(https://github.com/JuliaLang/julia/issues/23919#issuecomment -441267056,https://github.com/JuliaLang/julia/issues/23919#issuecomment-441824982)
mul!
之所以更好的另一个原因是...:
mul!
未记录,因此我们无需将其视为公共API。以及为什么muladd!
/ addmul!
更好是因为...:
mul!
和muladd!
/ addmul!
使用不同的三或四参数“方便函数”(https://github.com/JuliaLang/julia/issues /23919#issuecomment-402953987、https://github.com/JuliaLang/julia/issues/23919#issuecomment-431046516等)。 反论点:与mul!(y, A, x)
相比,写mul!(y, A, x, 1, 1)
并不冗长(https://github.com/JuliaLang/julia/issues/23919#issuecomment-430674934等)感谢您的客观总结@tkf
我也可以使用muladd!(A,B,C,α= true,β= false)来匹配muladd的签名。
我希望对于名为mulladd!
的函数,默认β=true
。 不过,我认为从muladd
决定的这种参数顺序相对于mul!(C,A,B)
会非常混乱
也许我弄错了,但我认为大多数人/应用程序/高级代码(仅对乘法运算符*
并不满意)需要mul!
。 那些知道矩阵乘法的BLAS API可以使人们在较低级别的代码中使用将βC
与β=1
( true
)或其他方式混合的功能这个。 我猜想这些人会在mul!
下寻找此功能,这是gemm
, gemv
,已建立的Julia界面... ...添加新名称(这令人困惑相反的论证顺序)似乎不值得; 我看不到收益吗?
我猜想这些人会在
mul!
下寻找此功能,这是gemm
,gemv
,已建立的Julia接口... ...添加新名称(这令人困惑相反的论证顺序)似乎不值得; 我看不到收益吗?
我认为可发现性不是一个大问题,因为我们可以在mul!
文档字符串中简单提及muladd!
。 那些精通BLAS的人会知道在哪里寻找API,对吧?
关于位置vs关键字参数:这里没有讨论,但是我认为C = αAB + βC
α
是对角矩阵的C = αAB + βC
可以像标量α
一样高效和容易地实现。 这种扩展要求我们可以分派α
的类型,而关键字参数是不可能的。
另外,对于非交换式Eltype,您可能希望通过调用muladd!(α', B', A', β', C')
(假设参数顺序)来有效地计算C = ABα + Cβ
。 它可能要求您能够在惰性包装Adjoint(α)
和Adjoint(β)
上调度。 (我个人没有在Julia中使用非可交换数字,因此这可能是非常假设的。)
我同意@Jutho的观点,即对于像库实现者这样的熟练程序员,此乘加函数是一个低级API。 我认为可扩展性对此API具有较高的优先级,而位置参数则是解决之道。
避免关键字参数的另一个参数是@andreasnoack在https://github.com/JuliaLang/julia/issues/23919#issuecomment -365762889之前所说的:
除非您知道BLAS,否则
α
和β
名称也不是很直观。
@tkf ,可以肯定的是,我的论点是: β != 0
的实际使用数量将比β == 0
的实际使用数量少,并且需要它的人不会惊讶地发现它稍微更通用mul!
下的行为。 因此,我看不到用新名称将其分离的好处,特别是因为参数顺序混乱了(至少使用muladd!
)。 如果必须使用新方法,我也会同情您的addmul!
。
那些需要它的人在
mul!
下发现这种稍微更普遍的行为不会感到惊讶。
我同意这一点。
因此,我看不出以新名称将其分开的好处,
除非您看到一些危害,否则我认为这是全球利益,因为其他人也看到了利益。
尤其是由于参数顺序混乱(至少有
muladd!
)
我想您认为这是有害的,我明白了。 只是我认为muladd!
/ addmul!
其他好处更重要。
我想您认为这是有害的,我明白了。 这只是我认为muladd!/ addmul!的其他好处。 更重要。
这确实是一种危害,再加上mul!
一直是与乘法相关的多个BLAS运算的唯一入口点,因为它没有完全访问α和β而受到限制。 现在有了muladd!
,将有两个不同的入口点,这取决于请求的操作中的细微差异,可以轻松地将其捕获为参数(实际上,可以由BLAS API中的参数捕获) 。 我认为首先不提供对BLAS API的完全访问权在Julia中是一个错误(因此,感谢您修复@tkf)。 尽管这是古老的可怕的fortran命名约定,但我想这些家伙知道为什么他们要这样做。 但是同样,我认为该操作家族(即由α和β参数化的2参数操作家族)在单个入口点下属于同一类,就像在BLAS中一样。
我认为最有效的计数器参数是是否将访问C
的原始数据之间的差异。 但是考虑到朱莉娅(Julia)接受乘以false
作为保证零结果的方法,即使其他因素是NaN
,我也认为这已得到解决。 但是也许这个事实需要更好地进行沟通/记录(自从阅读文档以来已经有一段时间了),而且我也是最近才知道这一点。 (这就是为什么在KrylovKit.jl中,我需要存在fill!
方法来用零初始化一个类似矢量的任意用户类型。但是现在我知道我可以改为rmul!(x,false)
,因此我无需强加实施fill!
)。
这只是我认为muladd!/ addmul!的其他好处。 更重要。
因此,让我扭转这个问题,拥有一种新方法还有哪些其他好处? 我再次阅读了您的摘要,但只看到了访问C
,我刚刚对其进行了评论。
我今天早上对我的妻子说过,朱莉娅社区进行了为期两个月的关于手术命名的对话。 她建议将其称为“弗雷德!” -没有缩写,没有深层含义,只是一个好名字。 只是代表她把它放在那里。
很好,她加上了感叹号! 😄
首先,以防万一,让我澄清一下,我所关心的几乎只是代码的可读性,而不是可写性或可发现性。 您只编写一次代码,但是却多次阅读。
拥有新方法的其他好处是什么?
正如您所评论的,我认为输出vs输入-输出参数参数是最重要的。 但这只是我认为_C =αAB+βC_与_C = AB_不同的原因之一。 我还认为,在前者的表达是后者的严格“超集”的意义上,它们是不同的简单事实需要在代码中清楚地进行视觉指示。 使用不同的名称可以帮助中级程序员(或几乎没有专心的高级程序员)略读代码并注意到它使用的东西比mul!
更奇怪。
我刚刚检查了一次民意调查(您需要单击上面的“加载更多”),看起来有些票从mul!
移到muladd!
吗? 上次我看到它时, mul!
赢了。 让我们在他们搬家之前在这里记录一下:笑:
mul!
:6addmul!
:2muladd!
:8更为严重的是,我仍然认为此数据并未显示mul!
或muladd!
比其他数据更清晰。 (尽管它表明addmul!
是少数:sob :)
感觉就像我们被困住了。 我们如何前进?
仅将其称为gemm!
吗?
只是称它为gemm! 代替?
我希望这是个玩笑...除非您为矩阵*向量提议gemm!(α, A::Matrix, x::Vector, β, y::Vector) = gemv!(α, A, x, β, y)
。
我们是否可以暂时保留已经存在的(稀疏矩阵) mul!
接口,以便我们可以合并PR并进行改进,并担心是否要在其他PR中添加muladd!
?
也许这里的每个人都已经清楚了,但我只是想强调一下投票不是
mul!
与muladd!
但
mul!
与( mul!
和muladd!
)即具有两个变异乘法函数而不是一个。
自从我投票赞成mul!
以来,我决定不再发表文章,投票似乎从mul!
变为( mul!
和muladd!
)。
但是,我有一个问题吗? 如果我们以当前的多数票通过,并且同时拥有mul!(C,A,B)
和muladd!(A,B,C,α=true,β=true)
,我想准备一个PR,用替换$$$ axpy!
和axpby!
一个更朱利安的名字add!
,应该是add!(y, x, α=true, β=true)
或add!(x, y, α=true, β=true)
(为清楚起见,其中y
是突变的)。 或者是其他东西?
万一不太明显, muladd!(A,B,C)
会违反变异参数优先的约定。
我们能否离开已经存在的
mul!
接口
@jebej我认为这个“向后兼容性”的论点被广泛讨论。 但是,它并不能说服任何人(通过民意调查,不仅是我)。
担心我们是否要在其他PR中添加
muladd!
?
破坏公共API很不好。 因此,如果我们说mul!
那么它将永远是mul!
(尽管从理论上讲,LinearAlgebra可以更改其主要版本以破坏API)。
我想准备一个PR,将
axpy!
和axpby!
替换为更朱利安的名字add!
,应该是add!(y, x, α=true, β=true)
或add!(x, y, α=true, β=true)
@Jutho谢谢,那太好了! 我认为,一旦我们决定了乘加API的调用签名,就可以轻松选择参数的顺序。
muladd!(A,B,C)
将违反约定,即变元优先。
@simonbyrne但是(正如您已经在https://github.com/JuliaLang/julia/issues/23919#issuecomment-426881998中提到的那样), lmul!
和ldiv!
变异了非第一个参数。 因此,我认为我们不必从选择中排除muladd!(A,B,C,α,β)
,而应将其视为此签名的负值。
(但是如果我们要使用“文本顺序” API,我会说要和muladd!(α, A, B, β, C)
一起使用。)
顺便说一句,投票结果我不了解的一件事是muladd!
和addmul!
的不对称性。 如果您写C = βC + αAB
,我认为看起来像addmul!
更自然。
@tkf关于您首先要执行的操作。 对我来说addmul!
建议您先加法,然后乘积,如(A+B)C
。 当然是主观的。 但是好名声应该吸引直觉。
嗯,我明白了。
由于此问题仍然存在,我的建议将使用模式包含功能定义,其中(第二个为@callexpr
)
@callexpr(C .= β*C + α*A*B) = implementation(C, β, α, A, B)
@callexpr(C .= β*C + A*B) = implementation(C, β, true, A, B)
也许是一种更易于派遣的表格(第二个是@callname
)
function @callname(β*C + A*B)(C::Number, β::Number, A::Number, B::Number)
β*C + A*B
end
和电话
@callexpr(A .= 2*C + A*B)
@callexpr(2*3 + 3*2)
无需担心(或不知道) callexpr
如何将代数运算转换成唯一的函数名称(该名称不取决于参数符号,而仅取决于运算和运算的顺序。)
我对实现做了一些思考,它应该是可行的。
@mschauer我认为这是一个有趣的方向。 你能打开一个新的问题吗? 您提出的API可以解决许多其他问题。 我认为它需要经过仔细的设计过程,而不是解决它可以解决的单个问题。
因此,我听说有传言说功能冻结1.1将在下周发布。 尽管下一个次要版本距离“只有”四个月了,但是如果我们可以在1.1中发布它,那将是非常好的……
无论如何,在合并PR之前,我们还需要确定呼叫签名(参数和关键字的顺序或非参数的顺序)。
因此,让我们再次投票(因为我发现这是一个很好的刺激因素)。
_如果_我们将muladd!
用于_C =ABα+Cβ_,那么您最喜欢的呼叫签名是什么?
muladd!(C, A, B, α, β)
muladd!(A, B, C, α, β)
muladd!(C, A, B; α, β)
(例如:+1 :,但带有关键字argumentsbs)muladd!(A, B, C; α, β)
(如:-1 :,但带有关键字参数)muladd!(A, B, α, C, β)
如果您有其他关键字参数名称,请使用α
和β
投票,然后评论使用哪种名称更好。
由于我们尚未决定名称应为什么,因此我们也需要为mul!
做它:
_如果_我们将mul!
用于_C =ABα+Cβ_,那么您最喜欢的呼叫签名是什么?
mul!(C, A, B, α, β)
mul!(A, B, C, α, β)
mul!(C, A, B; α, β)
(如:+1 :,但带有关键字argumentsbs)mul!(A, B, C; α, β)
mul!(A, B, α, C, β)
注意:我们不会更改现有的API mul!(C, A, B)
注意:我们不会更改现有的API
mul!(C, A, B)
我没有对这个事实给予足够的重视-我们已经有mul!
,这就是这个意思:
mul!(Y, A, B) -> Y
计算矩阵矩阵或矩阵向量乘积
A*B
并将结果存储在Y
,覆盖Y
的现有值。 请注意,Y
不得使用A
或B
别名。
鉴于此,像这样扩展它似乎很自然:
mul!(Y, A, B) -> Y mul!(Y, A, B, α) -> Y mul!(Y, A, B, α, β) -> Y
计算矩阵矩阵或矩阵向量乘积
A*B
并将结果存储在Y
,覆盖Y
的现有值。 请注意,Y
不得使用A
或B
别名。 如果提供了标量值α
,则将计算α*A*B
而不是A*B
。 如果提供了标量值β
,则将计算α*A*B + β*Y
。 相同的别名限制适用于这些变体。
不过,我觉得有这个的主要问题:它似乎至少在自然的mul!(Y, A, B, C, D)
计算A*B*C*D
到位为Y
-and是一般的概念冲突非常严重与mul!(Y, A, B, α, β)
计算α*A*B + β*C
。 而且,在我看来,将A*B*C*D
计入Y
是一件有用的事情,并且可以高效地执行,避免中间分配,所以我真的不想阻塞这个意思。 。
考虑到mul!
其他自然概括,这是另一个想法:
mul!(Y, α, A, B) # Y .= α*A*B
这适合于mul!(out, args...)
的通用模型,您可以在其中通过将args
乘以在一起来计算并写入out
。 它依靠调度来处理α
是标量,而不是使其成为特例,这只是您要乘的另一件事。 当α
是标量,而A
, B
和Y
是矩阵时,我们可以分派给BLAS进行超级高效的处理。 否则,我们可以有一个通用的实现。
此外,如果您位于非交换字段(例如四元数)中,则可以控制α
的缩放比例发生在哪一侧: mul!(Y, A, B, α)
缩放比例为α
右边而不是左边:
mul!(Y, A, B, α) # Y .= A*B*α
是的,我们不能为四元数调用BLAS,但是它是通用的,我们可能仍然可以相当有效地做到这一点(甚至可以通过某种方式将其转换为某些BLAS调用)。
假设采用Y .= α*A*B
的方法,下一个问题将变成:缩放和递增Y
怎么办? 我开始考虑为此使用关键字,但是随后想到的是非交换字段,感觉太尴尬和有限。 因此,我开始考虑使用此API,乍一看似乎有些奇怪,但请耐心等待:
mul!((β, Y), α, A, B) # Y .= β*Y .+ α*A*B
有点奇怪,但可以。 在非交换字段中,您可以要求将Y
乘以右边的β
,如下所示:
mul!((Y, β), α, A, B) # Y .= Y*β .+ α*A*B
在非交换字段中,您可以像这样在左右方向上进行缩放:
mul!((β₁, Y, β₂), α₁, A, B, α₂) # Y .= β₁*Y*β₂ + α₁*A*B*α₂
现在,当然,这有点不可思议,并且没有为此执行BLAS操作,但这是GEMM的概括,让我们表达了很多东西,我们可以琐碎地分派给BLAS操作,甚至不需要做任何讨厌的if / else分支机构。
我真的很喜欢@StefanKarpinski的建议作为基础API调用,但是我也在考虑这是否是我们实际上想要向用户公开内容的方式。 IMO,最后看起来应该很简单,就像一个关联的宏:
@affine! Y = β₁*Y*β₂ + α₁*A*B*α₂
然后,底层函数将类似于@StefanKarpinski提出的东西。
但是我们应该在这里走得更远。 我真的认为,如果为它创建一个API和一个泛型函数,那么有人会创建一个有效执行它的Julia库,所以我同意我们不应该只在这里坚持使用BLAS。 诸如MatrixChainMultiply.jl之类的东西已经在构建用于多矩阵计算的DSL,而DiffEq正在使用仿射运算符表达式来做自己的事情。 如果在Base中只有一个仿射表达式的表示形式,则可以将所有工作定义为同一事物。
@dlfivefifty之前
本质上,科学计算中的所有计算都可以归结为元素式和线性代数运算,因此,对这两者进行高层次的描述似乎有助于构建工具以进行元编程和探索新的设计。
我将不得不对该提案进行更多考虑,但就目前而言,我只是评论说,我认为您不希望在没有临时性的情况下计算A*B*C
。 在我看来,您必须付出大量的算术运算才能避免这种暂时的情况。
我认为您不希望在没有临时值的情况下计算
A*B*C
。
但是,对于mul!
您已经有一个输出数组。 我不确定是否有帮助。 无论如何,这似乎是一个实现细节。 API mul!(Y, A, B, C...)
表示您要计算的内容,并让实现选择最佳的方法来实现,这是此处的总体目标。
我真的很喜欢@StefanKarpinski的建议作为基础API调用,但是我也在考虑这是否是我们实际上想要向用户公开内容的方式。
@ChrisRackauckas :我认为您可以并且应该在外部程序包中探索的东西-懒惰,编写所需的计算并让某种优化通过挑选出与某些代数模式相匹配的部分,并且知道如何进行优化等。像这样使用mul!
似乎只是我们希望在此级别进行的一种通用但易于理解的操作。
请注意,关于mul!(Y, α, A, B)
并没有真正的争论-它几乎必须表示Y .= α*A*B
因为这还意味着什么? 因此,对我来说,这里唯一开放的问题是,使用带有矩阵和左和/或右标量的元组是否是表达我们要增加和缩放输出数组的合理方法。 一般情况为:
mul!(Y::Matrx, args...)
: Y .= *(args...)
mul!((β, Y)::{Number, Matrix}, args...)
: Y .= β*Y + *(args...)
mul!((Y, β)::{Matrix, Number}, args...)
: Y .= Y*β + *(args...)
mul!((β₁, Y, β₂)::{Number, Matrix, Number}, args...)
: Y .= β₁*Y*β₂ + *(args...)
第一个参数不允许有其他条件。 可以将其作为其他操作的更通用约定,在这些操作中可以覆盖或累加到输出数组中,并可以选择与缩放结合使用。
我没有想到要“合并” mul!(out, args...)
和类似GEMM的界面! 我喜欢它的可扩展性(但随后开始在下面写回复,现在不确定...)
但是我担心的是,是否易于用作重载接口。 我们需要依靠类型系统才能使嵌套元组正常工作。 在Julia的类型系统中,嵌套元组的工作方式与扁平元组一样好吗? 我想知道是否像“ Tuple{Tuple{A1,B1},C1,D1}
比Tuple{Tuple{A2,B2},C2,D2}
更具体,而Tuple{A1,B1,C1,D1}
比Tuple{A2,B2,C2,D2}
更具体”成立。 否则,用作重载API会很棘手。
请注意,我们确实需要分派标量类型以对复杂矩阵使用重新解释hack(这是来自PR#29634,所以不要注意函数名):
另一个担心是,这对于计算图执行器来说是一个有限的接口。 我认为乘加接口的主要目的是提供一个重载API,以使库实现者可以定义可以有效实现的小型可重用计算内核。 这意味着我们只能实现_C =ABα_而不能实现,例如_αAB_(请参阅https://github.com/JuliaLang/julia/pull/29634#issuecomment-443103667)。 为非交换型Eltype支持_α₁ABα_2_需要一个临时数组或增加算术运算的数量。 目前尚不清楚哪个用户想要,理想情况下这应该是可配置的。 此时,我们需要一个与执行机制分离的计算图表示形式。 我认为最好在外部包(例如LazyArrays.jl,MappedArrays.jl)中进行探索。 但是,如果我们能够找到在某个时候涵盖大多数用例的实现策略,则使用mul!
作为主要切入点是有意义的。 我认为这实际上是支持muladd!
另一个原因; 为将来的调用API分配空间。
我将不得不对该提案进行更多考虑,但就目前而言,我只是评论说,我认为您不希望在没有临时性的情况下计算A B C。 在我看来,您必须付出大量的算术运算才能避免这种暂时的情况。
您确实可以证明,任意数量张量的任何收缩始终是使用成对收缩来评估整个事物的最有效方法。 因此,将几个矩阵相乘只是其中的一种特殊情况,您应该将它们成对相乘(最佳顺序当然不是一个平凡的问题)。 这就是为什么我认为mul!(Y,X1,X2,X3...)
不是那么有用的原语。 最后,我认为这就是mul!
,这是开发人员可以为其特定类型重载的原始操作。 然后,可以使用更高级别的结构(例如使用宏)来编写任何更复杂的操作,并且可以例如构建一个计算图,最后通过调用诸如mul!
类的原始操作来评估该计算图。 当然,该原语可能足够笼统,可以包含@StefanKarpinski提到的非可交换的情况。
只要不涉及矩阵乘法/张量收缩,就可以根据原始操作进行思考并不是那么有用,并且像广播一样将所有内容融合在一起可能是有益的。
通常,我同意在Base中具有默认的惰性表示/计算图类型会很好,但是我认为mul!
是构建它的方法。
@tkf :
但是我担心的是,是否易于用作重载接口。 我们需要依靠类型系统才能使嵌套元组正常工作。 在Julia的类型系统中,嵌套元组的工作方式与扁平元组一样好吗?
是的,我们在这方面都很好。 我不确定嵌套的位置,但是在元组中传递某些东西与将它们全部传递为直接参数一样有效—它的实现方式完全相同。
这意味着我们只能实现_C =ABα_而不能实现,例如_αAB_
我很困惑...您可以写mul!(C, A, B, α)
和mul!(C, α, A, B)
。 您甚至可以编写mul!(C, α₁, A, α₂, B, α₃)
。 这似乎是迄今为止提出的最灵活的通用矩阵乘法API。
另一个担心是,这对于计算图执行器来说是一个有限的接口。 我认为乘加接口的主要目的是提供一个重载API,以使库实现者可以定义可以有效实现的小型可重用计算内核。
此时,我们需要一个与执行机制分离的计算图表示形式。
可能是这种情况,但这不是它的地方-可以并且应该在外部程序包中进行开发。 解决这个特定问题所需要做的就是使用矩阵乘法API,该API概括了可以分派给BLAS操作的内容,这几乎就是这样做的结果。
@朱索
因此,将几个矩阵相乘只是其中的一种特殊情况,您应该将它们成对相乘(最佳顺序当然不是一个平凡的问题)。 这就是为什么我认为
mul!(Y,X1,X2,X3...)
不是那么有用的原语。
mul!
操作将允许实现选择乘法顺序,这是一个有用的属性。 确实,能够执行此操作的能力是为什么我们首先将*
操作解析为n元,并且同样的原因甚至适用于mul!
因为如果您使用它, ,您大概已经足够在意性能。
通常,我无法确定您是赞成还是反对mul!
。
我不确定嵌套的位置,但是在元组中传递某些东西与将它们全部作为直接参数传递一样有效
我并不担心效率,而是担心调度和方法的歧义,因为即使当前的LinearAlgebra也有些脆弱(这可能是由于我对类型系统缺乏了解;有时使我感到惊讶)。 我之所以提到嵌套元组,是因为我认为方法的解析是通过将所有位置参数的元组类型相交来完成的。 那给你一个扁平的元组。 如果在第一个参数中使用元组,则将有一个嵌套元组。
这意味着我们只能实现_C =ABα_而不能实现,例如_αAB_
我很困惑...您可以写
mul!(C, A, B, α)
和mul!(C, α, A, B)
。
我的意思是说“我们只能像_C = AB_,而像_αAB_这样的其他候选对象不能在矩阵类型的所有组合中都得到有效实现。” (通过效率,我指的是O时间的复杂性。)我不确定这确实是事实,但至少对于稀疏矩阵,其他两种选择都没有了。
此时,我们需要一个与执行机制分离的计算图表示形式。
可能是这种情况,但这不是它的地方-可以并且应该在外部程序包中进行开发。
这正是我的意思。 我建议将此API作为此类用法的最小构建块(当然,这并不是全部目的)。 在人们探索了外部包装中的设计空间之后,即可完成vararg mul!
实现和设计。
甚至为当前mul!
分配类型的操作都已经“中断”:与诸如SubArray
和Adjoint
类的可组合数组类型一起使用时,模棱两可重写的组合增长。
解决方案是使用特征,LazyArrays.jl具有带有特征的mul!
概念证明版本。
但这更多是关于实现的讨论,而不是API。 但是使用元组对术语进行分组感觉不对:这不是类型系统的用处吗? 在这种情况下,您将获得LazyArrays.jl解决方案。
mul! 操作将允许实现选择乘法的顺序,这是一个有用的属性。 确实,有能力做到这一点,就是为什么我们首先将*操作解析为n元,而相同的推理甚至更适用于mul! 因为如果您正在使用它,那么您大概会在意性能。
*
解析为n
-ary非常有用。 我在TensorOperations.jl中使用它来实现@tensoropt
宏,该宏的确优化了收缩顺序。 我发现n
版本的mul!
没什么用,是因为从效率的角度来看,如果提供了所有中间值,则没有必要提供预先分配的位置来放置结果数组仍然必须在函数内部分配,然后进行gc'ed处理。 实际上,在TensorOperations.jl中,几个人注意到,大型临时人员的分配是Julia的gc确实表现不佳的地方之一(很多情况下,gc的使用率通常是50%)。
因此,如果我正确理解的话,我会将mul!
限制为真正的原始运算,正如@tkf所提倡的mul!
BLAS提供的功能(gemm,gemv,...)。
我不喜欢你对元组的建议,但可以预见可能会造成混乱
从案例4限制为案例2的3似乎意味着默认值β₁ = 1
和β₂ = 1
(或者实际上是true
)。 但是,如果两者均未指定,则突然意味着β₁ = β₂ = 0
( false
)。 当然,语法略有不同,因为您编写的是mul!(Y, args...)
,而不是mul!((Y,), args...)
。 最后,这只是文档问题,所以我只想指出这一点。
因此,总而言之,我不真正反对这种语法,尽管它是一种新型的范例,正在被引入,然后可能在其他地方也应遵循。 我所反对的是立即想将其推广为任意数量矩阵的乘法,如上所述,我看不出这样做的好处。
@dlfivefifty :但这是关于实现的讨论,而不是API。 但是使用元组对术语进行分组感觉不对:这不是类型系统的用处吗? 在这种情况下,您将获得LazyArrays.jl解决方案。
但是,我们不会在这里使用完整的惰性数组-为此已经有了LazyArrays。 同时,我们需要某种方式来表示缩放比例Y
。 使用元组似乎是表达这种少量结构的一种简单,轻便的方法。 还有其他建议吗? 我们可以为β₁
和β₂
设置lscale
和/或rscale
关键字,但是感觉并不优雅,我们将失去派遣相关人员,这并不重要,但是很高兴。
@Jutho :因此,如果我正确理解的话,我会将
mul!
限制为真正的原始运算,正如@tkf所提倡的:将两个矩阵乘以第三个矩阵,并可能带有标量系数。 是的,我们可以想到针对非交换代数执行此操作的最通用方法,但是目前,我认为当前的迫切需求是便捷地访问Julia的mul!
BLAS提供的功能(gemm,gemv,...)。
我只需要为mul!
定义一小部分操作就可以
# gemm: alpha = 1.0, beta = 0.0
mul!(Y::Matrix, A::Matrix, B::Matrix) # gemm! Y, A
# gemm: alpha = α, beta = 0.0 (these all do the same thing for BLAS types)
mul!(Y::Matrix, α::Number, A::Matrix, B::Matrix)
mul!(Y::Matrix, A::Matrix, α::Number, B::Matrix)
mul!(Y::Matrix, A::Matrix, B::Matrix, α::Number)
# gemm: alpha = α, beta = β (these all do the same thing for BLAS types)
mul!((β::Number, Y::Matrix), α::Number, A::Matrix, B::Matrix)
mul!((β::Number, Y::Matrix), A::Matrix, α::Number, B::Matrix)
mul!((β::Number, Y::Matrix), A::Matrix, B::Matrix, α::Number)
mul!((Y::Matrix, β::Number), α::Number, A::Matrix, B::Matrix)
mul!((Y::Matrix, β::Number), A::Matrix, α::Number, B::Matrix)
mul!((Y::Matrix, β::Number), A::Matrix, B::Matrix, α::Number)
# gemm: alpha = α, beta = β₁*β₂ (these all do the same thing for BLAS types)
mul!((β₁::Number, Y::Matrix, β₂::Number), α::Number, A::Matrix, B::Matrix)
mul!((β₁::Number, Y::Matrix, β₂::Number), A::Matrix, α::Number, B::Matrix)
mul!((β₁::Number, Y::Matrix, β₂::Number), A::Matrix, B::Matrix, α::Number)
为了什么目的为什么在表达BLAS操作方面有如此大的差异?
因为它允许人们表达自己的意图-如果意图是在左边或右边或两者上相乘,为什么不让人们表达自己的意图并选择正确的实现?
我们可以拥有通用后备,即使对于非可交换元素类型也可以做正确的事情。
这个问题的全部重点是归纳包含gemm的就地矩阵乘法! 同时比gemm更通用! 否则,为什么不继续写gemm!
呢?
但是我们不会在这里进行完整的惰性数组
我并不是说“完整的延迟数组”,而是建议延迟与Broadcasted
,后者最终会在编译时删除。 本质上,我将添加一个Applied
来表示函数的惰性应用,而不是使用元组(不包含上下文),您将得到
materialize!(applied(+, applied(*, α, A, B), applied(*, β, C)))
可以像广播的.*
标记一样涂糖,以使其更具可读性,但是与基于元组的提议不同,我认为这是显而易见的。
如前所述,
mul!((β₁::Number, Y::Matrix, β₂::Number), α₁::Number, A::Matrix, α₂::Number, B::Matrix, α₃::Number)
及其所有简化版本,即如果5个标量参数都可以不存在,则2 ^ 5 = 32种不同的可能性。 而且这结合了不同矩阵或向量的所有可能性。
我同意@dlfivefifty所说的类似广播的方法更可行。
是的,我意识到我遗漏了一些选项,但是32种方法对我来说似乎并不那么疯狂,毕竟我们不必手工编写它们。 添加一个“广播类的系统”或懒惰的评价体系,让我们写materialize!(applied(+, applied(*, α, A, B), applied(*, β, C)))
似乎是一个大得多的加法和出路的范围内针对此问题。 我们想要的是某种拼写通用矩阵乘法的方法,该方法既通用又让我们分派到BLAS。 如果我们不能完全同意这一点,那么我倾向于让人们继续直接致电gemm!
。
是的,那可能是真的。 我以为后面加标量参数会更容易提供默认值。 但是,如果使用某些@eval
元编程,我们可以轻松地生成所有32个定义,那同样好。 (请注意,您一定知道, mul
不仅是gemm!
而且还是gemv
和trmm
和...)。
让我补充一点,它不仅仅是BLAS包装器。 stdlib中还有其他纯Julia专用方法。 同样,将其作为重载API也很重要:包作者可以为其特殊的矩阵类型定义mul!
。
我想这是我的立场:
mul!(C, A, B, a, b)
因为它已经存在于SparseArrays.jl中Applied
类的广播,因为已经建立了设计模式。 这要等到Julia 2.0,LazyArrays.jl中仍会继续开发适合我需要的原型。@dlfivefifty您认为消除mul!((Y, β), α, A, B)
API歧义的难度等同于mul!(Y, A, B, α, β)
吗? 考虑诸如Transpose
类的矩阵包装器会引入难度,包括2和3元组的声音听起来像是增加难度(尽管我知道元组在Julia的类型系统中是特例)。
- 我们不妨现在支持
mul!(C, A, B, a, b)
因为它已经存在于SparseArrays.jl中
有人认为mul!(C, A, B, a, b)
应该意味着C .= b*C + a*A*B
而不一路思考,这一事实显然不是重复考虑的好理由。 如果mul!
是*
的就地版本,那么我看不到mul!(out, args...)
除了out .= *(args...)
之外还意味着什么。 坦白说,这就是您最终会得到一个系统的问题,该系统是一堆经过深思熟虑,不一致的API,这些API仅在历史偶然的情况下才存在。 mul!
函数不会从SparseArrays
_中导出,并且未记录该特定方法,因此,这实际上是最脆弱的原因,它巩固了一个可能是由于该函数不是不公开! 我建议我们撤消该错误,然后删除/重命名mul!
。
在其余的讨论中,我们似乎不应该做任何其他事情,因为所有利益相关者都想在标准库之外做一些具有特征和/或懒惰的事情。 我很好,因为删除东西总是很好。
在其余的讨论中,我们似乎不应该做任何其他事情,因为所有利益相关者都想在标准库之外做一些具有特征和/或懒惰的事情。 我很好,因为删除东西总是很好。
看来您有点烦了,这是可以理解的。 但是,我认为结论并不正确。 如果您确信当前建议可以以可扩展的方式实现,并且仍然使程序包开发人员可以方便地将其定义重载到自己的矩阵和向量类型(如@tkf所述),那将是一个不错的方法前锋。
特别是,我认为包开发人员仅需要实现:
mul!((β₁, Y::MyVecOrMat, β₂), α₁, A::MyMat, α₂, B:: MyVecOrMat, α₃)
也许,例如
mul!((β₁, Y::MyVecOrMat, β₂), α₁, A::Adjoint{<:MyMat}, α₂, B:: MyVecOrMat, α₃)
...
而Julia Base(或更确切地说,LinearAlgebra标准库)负责处理所有默认值等。
我认为软件包开发人员只需要实现:
mul!((β₁, Y::MyVecOrMat, β₂), α₁, A::MyMat, α₂, B:: MyVecOrMat, α₃)
我建议记录一下
mul!((Y, β), A, B, α)
作为要重载的签名。 这是因为α
其他位置会改变O时间的复杂性。 参见: https :
有一件事我喜欢@StefanKarpinski的做法是,我们可以实现对专业方法mul!((Y, β), α::Diagonal, A, B)
为_some_矩阵型A
(如Adjoint{_,<:SparseMatrixCSC}
不改变时间复杂度) 。 (这对我的应用程序很重要。)当然,要采用这种方式,需要在API中进行进一步的讨论,尤其是对于如何查询专用方法的存在。 尽管如此,有机会扩展API还是很棒的。
如果有人澄清了我对方法歧义的担心,那么我将全力以赴。
这是因为其他位置的α会改变O时间的复杂度。
这是稀疏矩阵吗? 我不太赞同这个观点,尤其是对于稠密矩阵。 在链接到的实现中,您将显示α
在A
和B
?
我认为包开发人员只需要实现...
这太过简单了。 假设我们有一个行为类似于跨步矩阵的矩阵,例如BlockArrays.jl中的PseudoBlockMatrix
。 为了完全支持gemm!
我们需要用(1)本身,(2) StridedMatrix
,(3) Adjoint
本身覆盖PseudoBlockMatrix
每个排列,(4) Transpose
S的本身,(5) Adjoint
S的StridedMatrix
,(6) Transpose
S的StridedMatrix
,可能还有其他。 这已经是6 ^ 3 = 216个不同的组合。 然后,您想支持trmm!
并且必须对UpperTriangular
, UnitUpperTriangular
,其伴随项,它们的转置等进行相同的处理。 然后gsmm!
与Symmetric
和Hermitian
。
但是在许多应用程序中,我们不仅要使用矩阵,而且还需要它们的子视图,尤其是对于要使用块的块矩阵。 现在,我们需要添加矩阵的所有视图组合以及上述6种组合。
现在,我们有成千上万个覆盖,涉及StridedMatrix
,这是一个非常复杂的联合类型。 对于编译器而言,这太多了,这会使using
时间花费几分钟而不是几秒钟。
在这一点上,人们意识到当前的mul!
以及扩展的mul!
提议的扩展在设计上存在缺陷,因此程序包开发人员应该不必理会它。 幸运的是,LazyArrays.jl提供了使用特征的临时解决方法。
因此,我对@StefanKarpinski表示同意,只是将它们保持原样,直到进行更大规模的重新设计为止,因为将精力花在设计上有缺陷的东西上并不是浪费任何人的时间。
@dlfivefifty ,我仅指的是如何处理标量参数。 当然,对于mul!(C,A,B)
当前已经存在的具有不同矩阵类型的所有并发症。
这是因为其他位置的α会改变O时间的复杂度。
这是稀疏矩阵吗? 我不太赞同这个观点,尤其是对于稠密矩阵。 在链接到的实现中,您将显示
α
在A
和B
?
@Jutho我认为通常不能将α
放在最内层的循环位置。 例如,在这种情况下,您可以支持α₁*A*B*α₃
但不能支持A*α₂*B
我认为α₁
中的至少α₂
α₁*A*α₂*B*α₃
必须为1
以避免增加渐近时间复杂度。
@dlfivefifty但是,即使LazyArrays.jl也需要一些原始函数来分派,对吗? 我的理解是,它可以解决“调度难题”,但不会减少人们必须实现的计算“内核”数量。
不,没有“原语”,就像Broadcasted
没有“原语”一样。 但是,是的,目前它还不能解决“内核”问题。 我认为下一步是重新设计它使用一个懒惰的Applied
类型与ApplyStyle
。 然后可能会有MulAddStyle
来识别类似于BLAS的操作,而顺序无关紧要。
我将materialize!
或copyto!
称为原语。 至少这是广播机制的基础。 同样,我认为LazyArrays.jl必须在某个时候将其惰性表示降低为具有循环的函数或ccall
到外部库的功能,对吗? 如果该函数的名称为mul!
好吗?
这太过简单了。 假设我们有一个行为类似于跨步矩阵的矩阵,例如BlockArrays.jl中的PseudoBlockMatrix。 完全支持gemm! 我们需要使用(1)本身,(2)StridedMatrix,(3)自身的伴随物,(4)自身的转置,(5)StridedMatrix的伴随物,(6)StridedMatrix的转置以及其他可能覆盖PseudoBlockMatrix的每个置换。 这已经是6 ^ 3 = 216个不同的组合。 那你想支持trmm! 并且您必须对UpperTriangular,UnitUpperTriangular,其伴随项,它们的转置等进行相同的操作。 然后gsmm! 与Symmetric和Hermitian。
但是在许多应用程序中,我们不仅要使用矩阵,而且还需要它们的子视图,尤其是对于要使用块的块矩阵。 现在,我们需要添加矩阵的所有视图组合以及上述6种组合。
现在我们有成千上万个覆盖,涉及StridedMatrix,这是一个非常复杂的联合类型。 对于编译器来说,这太多了,使得使用时间要花几分钟而不是几秒钟。
我当然同意当前的StridedArray
类型联合是一个主要的设计缺陷。 我支持您在某个时候解决此问题的尝试。
在Strided.jl,仅当涉及的所有矩阵均为我自己的自定义类型(Abstract)StridedView
时,我才实现mul!
,只要A,B和C类型存在某种混合,我就让Julia Base / LinearAlgebra对此进行处理。 当然,这将在@strided
宏环境中使用,该环境试图将所有可能的Base类型转换为StridedView
类型。 在这里, StridedView
可以代表子视图,转置和伴随以及某些重塑,所有这些都具有相同(参数)类型。 总体而言,完整的乘法代码约为100行:
https://github.com/Jutho/Strided.jl/blob/master/src/abstractstridedview.jl#L46 -L147
万一BLAS不适用,本地Julia的后备是使用该软件包提供的更通用的mapreducedim!
功能实现的,并且效率不低于LinearAlgebra
; 但它也是多线程的。
我认为
α₁
中的至少α₂
α₁*A*α₂*B*α₃
必须为1,以避免增加渐近时间复杂度。
@tkf ,我假设如果这些标量系数采用默认值one(T)
,或者更好的是true
,则常量传播和编译器优化将自动消除该乘法。 在无操作的最内层循环中。 因此,仅需定义最通用的形式仍然很方便。
我不确定是否可以依靠常量传播来消除1
( true
)的所有乘法。 例如,eltype可以是Matrix
。 在那种情况下,我认为true * x
(其中x::Matrix
)必须创建x
的堆分配副本。 朱莉娅可以做些魔术来消除它吗?
@Jutho我认为此基准表明在某些情况下Julia无法消除中间乘法:
function simplemul!((β₁, Y, β₂), α₁, A, α₂, B, α₃)
<strong i="7">@assert</strong> size(Y, 1) == size(A, 1)
<strong i="8">@assert</strong> size(Y, 2) == size(B, 2)
<strong i="9">@assert</strong> size(A, 2) == size(B, 1)
<strong i="10">@inbounds</strong> for i in 1:size(A, 1), j = 1:size(B, 2)
acc = zero(α₁ * A[i, 1] * α₂ * B[1, j] * α₃ +
α₁ * A[i, 1] * α₂ * B[1, j] * α₃)
for k = 1:size(A, 2)
acc += A[i, k] * α₂ * B[k, j]
end
Y[i, j] = α₁ * acc * α₃ + β₁ * Y[i, j] * β₂
end
return Y
end
function simplemul!((Y, β), A, B, α)
<strong i="11">@assert</strong> size(Y, 1) == size(A, 1)
<strong i="12">@assert</strong> size(Y, 2) == size(B, 2)
<strong i="13">@assert</strong> size(A, 2) == size(B, 1)
<strong i="14">@inbounds</strong> for i in 1:size(A, 1), j = 1:size(B, 2)
acc = zero(A[i, 1] * B[1, j] * α +
A[i, 1] * B[1, j] * α)
for k = 1:size(A, 2)
acc += A[i, k] * B[k, j]
end
Y[i, j] = acc * α + Y[i, j] * β
end
return Y
end
fullmul!(Y, A, B) = simplemul!((false, Y, false), true, A, true, B, true)
minmul!(Y, A, B) = simplemul!((Y, false), A, B, true)
using LinearAlgebra
k = 50
n = 50
A = [randn(k, k) for _ in 1:n, _ in 1:n]
B = [randn(k, k) for _ in 1:n]
Y = [zeros(k, k) for _ in 1:n]
<strong i="15">@assert</strong> mul!(copy(Y), A, B) == fullmul!(copy(Y), A, B) == minmul!(copy(Y), A, B)
using BenchmarkTools
<strong i="16">@btime</strong> mul!($Y, $A, $B) # 63.845 ms (10400 allocations: 99.74 MiB)
<strong i="17">@btime</strong> fullmul!($Y, $A, $B) # 80.963 ms (16501 allocations: 158.24 MiB)
<strong i="18">@btime</strong> minmul!($Y, $A, $B) # 64.017 ms (10901 allocations: 104.53 MiB)
不错的基准。 我也已经注意到,通过一些类似的实验,它的确不会消除这些分配。 在这种情况下,定义特殊用途的One
单例类型可能会很有用,该类型仅定义*(::One, x::Any) = x
和*(x::Any, ::One) = x
,并且现在不需要用户类型。 这样,默认值(至少对于α₂
)可以是One()
。
啊,是的,这很聪明! 我首先以为我现在可以支持α₁ * A * α₂ * B * α₃
但是我想我发现了另一个问题:从数学上讲,当A
是矩阵矩阵并且α₁
是一个矩阵。 如果我们_never_在α
位置支持非标量参数,那将不是问题。 然而,这使得它不可能出现Y .= β₁*Y*β₂ + *(args...)
为一体的心智模式mul!((β₁, Y, β₂), args...)
。 此外,将对角矩阵传递给α₁
或α₂
真是太好
(1)使用mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃)
但是当重载该方法时,参数α
和β
必须接受Diagonal
。 定义调用路径很容易,以便最终用户代码仍可以通过标量值来调用它。 但是,要使其有效运行,必须在LinearAlgebra中实现Diagonal(fill(λ, n))
https://github.com/JuliaLang/julia/pull/30298#discussion_r239845163的“ O(1)版本”。 请注意,标量和对角线α
的实现没有太大区别; 通常只是交换α
和α.diag[i]
。 因此,我认为这对软件包作者没有太大的负担。
这解决了我上面提到的歧义,因为现在当A
是矩阵矩阵而α
是应视为eltype
矩阵时,您可以调用mul!(Y, α * I, A, B)
eltype
到A
。
(2)也许上述路线(1)仍然太复杂? 如果是这样,请暂时使用muladd!
。 如果我们想支持mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃)
,则在保持向后兼容性的同时迁移到它并不困难。
在这一点上,我想知道我们是否不应该只具有一个受限制且专门化的matmul!(C, A, B, α, β)
,而该
matmul!(
C :: VecOrMatT,
A :: Matrix{T},
B :: VecOrMatT,
α :: Union{Bool,T} = true,
β :: Union{Bool,T} = false,
) where {
T <: Number,
VecOrMatT <: VecOrMat{T},
}
而且,可以签名并分发此签名确实令人惊讶。
这本质上是我的建议(2),对吗? (我猜A :: Matrix{T}
并不意味着Core.Array{T,2}
;否则它或多或少只是gemm!
)
作为临时解决方案,我会感到满意,并且可以在我维护的软件包中部分支持它(由于突出的特征问题,因此是“部分”),尽管它为组合添加了另一个名称: mul!
, muladd!
和现在matmul!
。
...是不是时候有人张贴“分类审理说...”并打电话了?
而且,可以签名并分发此签名确实令人惊讶。
您不是可以在此签名上明确分配一个参数以使其成为mul!
方法的事实。 然后,在出现一些更一般的解决方案时,也可以彻底弃用它。
使其成为
mul!
如果我们使用mul!(C, A, B, α, β)
,则无法在不破坏兼容性的情况下将其推广到mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃)
之类。 (也许这是一个“功能”,因为我们永远不受讨论的困扰:微笑:)。
另外,让我注意到,将元素类型限制为Number
并保持与当前3-arg mul!
(已经支持非Number
元素类型)的兼容性会带来很多重复。
我不知道您期望mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃)
会做什么...所以我认为这是一个功能。
磕碰。 这让我伤心到不得不使用BLAS功能处处以获得更好的就地性能。
@StefanKarpinski能否请您分流?
我们可以讨论,尽管我不确定在没有电话联络的情况下会得到什么结果,而这些日子通常都没有。 坦率地说,我花费了大量的时间和精力来尝试使讨论达到某种解决方案,并且每个想法似乎都被一种或另一种方式坚定地拒绝了,所以我几乎在以下时间检查了此问题:这点。 如果有人可以对问题进行总结,以及各种建议为何不充分,那么这将有助于进行富有成效的分类讨论。 否则,我认为我们将无能为力。
我认为缺乏共识意味着现在不是将其整合到StdLib中的合适时机。
为什么不只是包MatMul.jl来实现停机用户可以使用的建议之一呢? 我不明白为什么在实践中在StdLib中如此重要。 我将在我维护的软件包中对此提供支持。
我在想只是一个不错的朱利安版本的gemm! 和gemv! 匹配我们已经在SparseArrays中拥有的东西。 根据上述@andreasnoack :
我以为我们已经将
mul!(C, A, B, α, β)
设置为α
,β
默认值。 我们正在使用此版本julia / stdlib / SparseArrays / src / linalg.jl
b8ca1a4中的第32至50
...
我认为有些软件包也在使用这种形式,但我不记得哪个在我头上。
这个建议有7个大拇指,没有大拇指。 我们为什么不只为密集的矢量/矩阵实现该功能? 那将是一个涵盖最常见用例的简单解决方案,对吗?
好。 因此,我想甚至没有共识:sweat_smile:
_I_认为几乎每个人[*]都希望使用此API,而这仅取决于函数名称和签名。 与拥有此API相比,我认为每个人都对任何选项感到满意(例如mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃)
, muladd!(C, A, B, α, β)
和mul!(C, A, B, α, β)
)。 除非有人能够令人信服地提出某个API比拥有它更糟糕的说法,否则我对分类会做出的决定感到满意。
@StefanKarpinski但是,如果您认为讨论不够充分,请随时删除triage
标签。
[*]好吧, @dlfivefifty ,我想您甚至对当前的3-arg mul!
也感到怀疑。 但这需要从头开始更改3-arg mul!
接口,因此我认为这超出了本讨论的范围(我一直在解释为_adding_某种形式的5-arg变体)。 我认为,在LazyArrays.jl成熟之前,我们需要“足够”有效的东西。
为什么不只是包MatMul.jl来实现停机用户可以使用的建议之一呢?
@dlfivefifty我认为在LinearAlgebra.jl中使用它很重要,因为它是一个接口函数(可重载的API)。 另外,由于mul!(C::AbstractMatrix, A::AbstractVecOrMat, B::AbstractVecOrMat)
是在LinearAlgebra.jl中实现的,因此我们将无法根据MatMul.muladd!
来定义mul!
MatMul.muladd!
。 当然,有一些解决方法,但是拥有一个简单的实现会更好,尤其是考虑到“仅”它需要确定名称和签名。
我们为什么不只为密集的矢量/矩阵实现该功能?
@chriscoey不幸的是,这并不是每个人唯一的最爱: https : 网址为https://github.com/JuliaLang/julia/issues/23919#issuecomment -441865841。 (也请参见其他人的评论)
从分类:从长期计划来看,具有通用张量收缩API,包括对编译器的支持和对BLAS的选择,但就中期而言,只需选择满足您当前需求的任何API。 如果匹配BLAS,则选择BLAS名称似乎是合理的。
@Keno ,您可以共享有关通用张量收缩API和编译器支持的任何信息吗? 我可能还需要分享一些有趣的信息,尽管还没有公开。
没有任何一项API设计已经完成,只是我们应该拥有的一般意义。 我知道您已经在做一些这样的事情,因此在适当的时候召开设计会议会很好,但是我认为我们还没有到那里。
如果匹配BLAS,则选择BLAS名称似乎是合理的。
这与我们迄今为止对通用线性代数函数名称所做的完全相反。
建议的一般版本BLAS.gemm!(α, A, B, β, C)
强/弱β == 0
的计划是什么?
如果我们降低到BLAS
调用,即使它现在与lmul!
不一致,它的行为也将像一个强零。 除了$$$ β == 0
降到generic_muladd!
之外,我想不出解决方案。
强/弱β== 0的计划是什么
在https://github.com/JuliaLang/julia/issues/23919#issuecomment -430139849中我的评论仅作了简要讨论,因此分类可能未解决该问题。
@Keno尽管还没有任何API设计,但您是否设想将“包括编译器支持和选择到BLAS的API”定义为变异的或不可变的,例如XLA线性代数,以帮助编译器? 即您认为mul!
和/或muladd!
将成为此类API的一部分吗?
平@Keno为@andreasnoack的问题在https://github.com/JuliaLang/julia/issues/23919#issuecomment -475534454
抱歉,我本来打算再谈这个问题(尤其是我要求进行分类),但我不知道分类会决定下一步采取什么行动。
如果匹配BLAS,则选择BLAS名称似乎是合理的。
正如@andreasnoack所指出的,我们不能使用(例如) gemm!
因为我们要支持矩阵向量乘法等。但是我想我们可以忽略这个分类决定(它只是说“如果它匹配BLAS” ;不是)。
只需选择满足您当前需求的任何API。
因此,我想我们可以遵循这个方向。 我认为这意味着忘记@StefanKarpinski建议的基于元组的API,而“只是”选择mul!
/ muladd!
/ addmul!
。
我们有点回到最初的讨论。 但我认为我们有一个约束,不再讨论API是件好事。
知道如何从mul!
/ muladd!
/ addmul!
选择一个名字吗?
@chriscoey我认为最好在其他地方讨论未来的API。 这个问题已经非常漫长了,除非我们专注于中期解决方案,否则我们将无法取得任何进展。 如何打开一个新的问题(或话语线程)?
我建议从现在起10天前完成一轮批准投票。 批准投票是指:每个人都对他们认为比继续讨论更可取的所有选择进行投票。 宁愿现在拥有其最不喜欢的名字而不是继续讨论的人也应该对这三个人投赞成票。 如果没有任何一种选择获得广泛认可,或者投票制度本身未能获得广泛认可,那么我们必须继续进行讨论。 如果批准的选项之间几乎有联系, @ tkf可以做出决定(PR作者的特权)。
+1:我同意这个投票方案,并投了我的赞成票。
-1:我不同意这个投票方案。 如果太多或太重要的人选择此选项,则投票没有意义。
心: mul!
比继续讨论更可取。
火箭: muladd!
比继续讨论更可取。
Hooray: addmul!
比继续讨论更可取。
我暂时建议75%的赞成票和5票绝对应达到法定人数(即,有75%的人已经投票,包括不同意整个投票程序,并且至少有5人批准了获胜方案;如果参与度很低) ,则5/6或6/8达到法定人数,但一致的4/4可被视为失败)。
1.3版的功能冻结于8月15日前后: //discourse.julialang.org/t/release-1-3-branch-date-approaching-aug-15/27233?u = chriscoey。 希望到那时可以将它合并😃。 感谢所有已经投票的人!
我们还需要确定β == 0
https://github.com/JuliaLang/julia/issues/23919#issuecomment -475420149的行为,该行为与确定名称正交。 同样,我的PR中的合并冲突必须解决,PR需要对实现细节进行一些审查(例如,我在那里处理目标数组中的undef
s的方法)。 我们可能会在审核过程中发现其他问题。 因此,我不确定是否可以达到1.3 ....
回复: β == 0
,我认为@andreasnoack的评论https://github.com/JuliaLang/julia/issues/23919#issuecomment -430139849(我的摘要: β == 0
应该是BLAS -尽可能利用BLAS兼容)是有意义的。 除了下面的@simonbyrne的参数外,很难找到相反的观点(“每个实现都需要一个分支来检查零”)。 还有其他反对类似BLAS的β == 0
处理的论点吗?
@simonbyrne关于您的评论https://github.com/JuliaLang/julia/issues/23919#issuecomment -430375349,我不认为显式分支是一个大问题,因为它主要只是一个单线性β != 0 ? rmul!(C, β) : fill!(C, zero(eltype(C)))
。 另外,对于需要处理的非常通用的实现,例如C = Matrix{Any}(undef, 2, 2)
,该实现无论如何都需要显式的“强零”处理(请参阅我的PR https:// github中的辅助函数_modify!
.com / JuliaLang / julia / pull / 29634 / files#diff-e5541a621163d78812e05b4ec9c33ef4R37)。 因此,我认为类似BLAS的处理是此处的最佳选择。 你怎么看?
高性能弱零点可能吗? 从某种意义上说,我们希望:
julia> A = [NaN 0;
1 0]
julia> b = [0.0,0];
julia> 0.0*A*b
2-element Array{Float64,1}:
NaN
0.0
julia> false*A*b
2-element Array{Float64,1}:
0.0
0.0
也就是说,如果我们降低到BLAS(使用强0),则需要手动确定哪些行应为NaN
。
@dlfivefifty我认为BLAS可以处理A
和B
NaN,但不能处理C
NaN?
我想没有办法使用BLAS.gemm!
等有效地执行NaN感知_C =α* A * B + 0 * C_ [1],这就是@andreasnoack的论点来自哪里。
[1]您需要将isnan.(C)
保存C
@chethega投票以来已有10天以上
@chriscoey你是对的,投票已经关闭。
我在github上太糟糕了,无法获得完整的投票者名单(这将需要计算有多少人投票)。 但是,当我查看这些数字时,很明显mul!
具有压倒性的支持(大概管理75%的批准定额),第二个竞争者muladd!
远远低于50%。
没有人对投票制度有任何异议。 我投票, mul!
赢了,名字确定了。 @tkf可以继续使它飞行:)
@chethega谢谢,这是个不错的选择!
顺便说一句,我无法立即(但可能要在几周内)进行重新设置基准,因此,如果有人想进行重新设置或重新实施,请不要等待我。
不幸的是,我们没有对NaN语义进行投票。 功能冻结将在下周进行,我们没有足够的时间进行有意义的投票。
我建议我们有一个不具约束力的全民投票,以收集线程中情绪的快照。
我看到以下选项:
mul!
被延迟到1.4。)NaN
表示NaN
! 合并1.3,并记录下对弱零的承诺。 :眼睛!(alpha === false) && iszero(alpha) && !all(isfinite, C) && throw(ArgumentError())
合并,并记录此错误检查可能会因其他原因而退出。 :困惑:可以随意选择多个可能相互矛盾的选项, @ tkf / triage可以随意忽略民意测验。
编辑:当前仅:tada:(耐心)和:rocket:(不耐烦)是矛盾的,但两者都与所有其他兼容。 如明确的下线所示,分类将希望对在14日星期三至15日星期四之间某个未指定日期的投票结果进行计数,并以某种未指定的方式将其考虑在内。 这又称为“批准投票”,即选择您喜欢的所有选项,而不仅仅是您喜欢的选项; 可以理解,:rocket:不赞成:thumbsup :、:heart :、:eyes:和: confused:。 抱歉,这次民意调查比上一次更匆忙。
如果它是“为1.x合并”而不是“为1.3合并”,我会投票赞成强零(:heart :)。 如果所有“合并为1.3”都是“合并1.x”,这些选项是否有意义?
谢谢@chethega。 @tkf我真的很需要新的mul! 尽快不关心NaN决策(只要不影响性能)。
您是否签出了LazyArrays.jl? 仅供参考,它具有非常好的融合矩阵乘法支持。 您也可以安全地使用BLAS.gemm!
等,因为它们是公共方法https://docs.julialang.org/en/latest/stdlib/LinearAlgebra/#LinearAlgebra.BLAS.gemm!
我实际上真的需要通用mul! 由于我们使用各种结构化,稀疏和普通旧式密集矩阵来最有效地表示不同的优化问题。 我在这里是为了通用性和速度。
我懂了。 我只是记得我们在LazyArrays.jl中讨论过事情,所以您当然已经知道了...
关于“尽快”, Julia的四个月发布周期至少是作为设计,以避免在功能冻结之前“合并急件”。 我知道这么说对我来说是不公平的,因为我之前曾尝试过相同的事情……但是我认为有人需要提起这个作为提醒。 好的一面是Julia超级容易构建。 您可以在合并后直到下一个版本立即开始使用它。
编辑:发布->合并
谢谢。 我发现截止日期是有益的激励因素,并且我想避免让它再次过时。 因此,我建议我们尝试将截止日期作为目标。
您正在为此线程积极注入能量真是太好了!
我实际上真的需要通用mul! 由于我们使用各种结构化,稀疏和普通旧式密集矩阵来最有效地表示不同的优化问题。 我在这里是为了通用性和速度。
五种参数mul!
在具有许多不同类型时将不能很好地工作:您将需要组合使用许多覆盖来避免歧义。 这是LazyArrays.jl MemoryLayout
系统背后的动机之一。 正是由于这个原因,它用于BandedMatrices.jl和BlockBandedMatrices.jl中的“结构化和稀疏”矩阵。 (甚至在这里将带状矩阵的子视图分配到带状BLAS例程。)
谢谢,我将再次尝试LazyArrays。
由于5-arg mul似乎通常被认为是暂时的权宜之计(直到可以在2.0中使用诸如LazyArrays之类的解决方案),所以我认为我们可以将其合并,而从长远来看不一定是理想或完美的解决方案。
@chethega ,您认为我们什么时候应该对新的不具约束力的投票进行计数?
@tkf当然,对于1.x,强/弱/不定零也是正确的。
但是,我认为很多人宁愿现在拥有1.3 mul!
不是等到1.4来获得5-arg mul!
。 如果没有截止日期,那么我将等待更多时间,并花更多时间思考如何进行适当的民意测验(至少需要10天的投票时间)。 最重要的是,如果不先针对弱零/强零的速度和优雅性提出和基准化竞争的实现,我们就无法进行有意义的投票。 我个人怀疑,通过首先检查iszero(alpha)
,然后在矩阵中扫描!isfinite
值,然后才使用带有额外分配的慢路径,可以使弱零几乎与强零一样快。 但我还是更喜欢强零语义。
@chethega ,您认为我们什么时候应该对新的不具约束力的投票进行计数?
分流必须在本周为1.3 alpha做出决定(延迟/强/弱/后退)。 我认为星期四15日或星期三14日是进行分类的明智选择,并且要考虑在内。 我可能无法在星期四加入,所以其他人将不得不数。
实际上,在这里保守一点,错过最后期限,继续讨论并等待1.4是可以的。
另一方面,我们可能已经在没有注意到的情况下达成共识: @andreasnoack提出了一些有力的论据,认为零系数应该是一个强零。 他很有可能说服了所有弱势的零拥护者。 很有可能是大多数人,尤其是去年,确实想要5-arg mul !,而实际上并不关心这个小细节。 如果真是这样,那么可惜进一步推迟该功能,只是因为没有人希望结束讨论。
为什么不现在就抛出一个错误:
β == 0.0 && any(isnan,C) && throw(ArgumentError("use β = false"))
为什么不现在就抛出一个错误
我在投票中添加了该选项。 伟大的折衷方案!
只是为了设置期望:1.3版本的功能冻结将在三天内完成,因此基本上不可能及时实现。 对于功能冻结和分支,我们非常严格,因为这是我们真正可以控制发布时间的唯一部分。
该工作已在https://github.com/JuliaLang/julia/pull/29634中完成。 只需进行调整和重新设置基准即可。
@tkf对于#29634,您能否列出尚待完成的工作(包括根据表决重命名和处理零)? 我知道您很忙,所以也许我们可以找到一种方法来拆分剩余的待办事项,以免负担再一次落在您身上。
我能想到的ATM的TODO是:
addmul!
-> mul!
mul!
(例如<strong i="15">@deprecate</strong> mul!(C, A, B, α, β) addmul!(C, A, B, α, β)
)并调整代码MulAddMul
等的实施策略的信息; 参见https://github.com/JuliaLang/julia/pull/29634#issuecomment -440510551。 @andreasnoack ,您有机会看看吗?Quaternion
添加测试; 参见https://github.com/JuliaLang/julia/pull/29634#issuecomment -443379914我的PR实现了β = 0
处理的BLAS语义。 因此,还必须执行其他处理,例如引发错误。
我的PR实现了
β = 0
处理的BLAS语义。
抱歉,我的记忆很陈旧; 我的实现不一致,并且传播NaN _sometimes_。 因此,另外的TODO是使β = 0.0
的行为保持一致。
MulAddMul
类型仅供内部使用,对吗?
是的,这完全是内部细节。 我担心的是(1)专业化可能太多(在type参数中编码了beta = 0等),以及(2)它降低了源代码的可读性。
这些是有效的担忧。 我们已经在线性代数代码中产生了大量的专业知识,因此很想一想我们是否真的需要在这里进行专业化。 我的想法通常是,我们应该针对小型矩阵进行优化,因为它不是免费的(如您所说,它会使源代码复杂化并可能增加编译时间),人们最好使用StaticArrays
进行小型矩阵乘法。 因此,我倾向于只在运行时检查值,但是如果我们改变主意,我们以后可以随时进行调整,这样就不会造成任何延迟。
FYI软零点确实具有简单的实现:
if iszero(β) && β !== false && !iszero(α)
lmul!(zero(T),y) # this handles soft zeros correctly
BLAS.gemv!(α, A, x, one(T), y) # preserves soft zeros
elseif iszero(α) && iszero(β)
BLAS.gemv!(one(T), A, x, one(T), y) # puts NaNs in the correct place
lmul!(zero(T), y) # everything not NaN should be zero
elseif iszero(α) && !iszero(β)
BLAS.gemv!(one(T), A, x, β, y) # puts NaNs in the correct place
BLAS.gemv!(-one(T), A, x, one(T), y) # subtracts out non-NaN changes
end
@andreasnoack抱歉,我忘了我们实际上需要专门化才能为某些结构化矩阵(如mul!(C, A::BiTriSym, B, α, β)
https://github.com/JuliaLang/julia/pull/29634#issuecomment -440510551)优化最内层循环。 可以删除一些专业化知识,但这实际上是更多工作(因此有所延迟)。
因此,我倾向于只在运行时检查值,但是如果我们改变主意,我们以后可以随时进行调整,这样就不会造成任何延迟。
大!
@andreasnoack非常感谢您及时审查和合并!
现在,它已合并为1.3,它开始使我对实现感到非常紧张:微笑:。 我很感激1.3-rc发行后,这里的人们可以更彻底地测试他们的线性代数代码!
无需担心,1.3 RC + PkgEval将有足够的时间来消除漏洞。
最有用的评论
我建议从现在起10天前完成一轮批准投票。 批准投票是指:每个人都对他们认为比继续讨论更可取的所有选择进行投票。 宁愿现在拥有其最不喜欢的名字而不是继续讨论的人也应该对这三个人投赞成票。 如果没有任何一种选择获得广泛认可,或者投票制度本身未能获得广泛认可,那么我们必须继续进行讨论。 如果批准的选项之间几乎有联系, @ tkf可以做出决定(PR作者的特权)。
+1:我同意这个投票方案,并投了我的赞成票。
-1:我不同意这个投票方案。 如果太多或太重要的人选择此选项,则投票没有意义。
心:
mul!
比继续讨论更可取。火箭:
muladd!
比继续讨论更可取。Hooray:
addmul!
比继续讨论更可取。我暂时建议75%的赞成票和5票绝对应达到法定人数(即,有75%的人已经投票,包括不同意整个投票程序,并且至少有5人批准了获胜方案;如果参与度很低) ,则5/6或6/8达到法定人数,但一致的4/4可被视为失败)。