<table border="1" cellspacing="0" cellpadding="8">
<tr>
<th>Issue</th>
<td>
<a href=https://github.com/llvm/llvm-project/issues/118433>118433</a>
</td>
</tr>
<tr>
<th>Summary</th>
<td>
[AVX-512] Missed `vpmaddwd` and `vpdpwssd` canonicalization
</td>
</tr>
<tr>
<th>Labels</th>
<td>
new issue
</td>
</tr>
<tr>
<th>Assignees</th>
<td>
</td>
</tr>
<tr>
<th>Reporter</th>
<td>
Validark
</td>
</tr>
</table>
<pre>
[Godbolt link](https://zig.godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYgAzKVpMGoAF55gpJfWQE8Ayo3QBhVLQCuLBiABMXUk4AMngMmAByngBGmMQSBgAOqAqE9gyuHl6%2B/onJdgLBoREs0bFcBtaYtqlCBEzEBOme3n5WmDZ5DDV1BAXhUTFxVrX1jZktCsM9IX3FA2UAlFao7sTI7BxoDBMA1BPo2wCkegAi2wACeCyJ9RAHPj57dz7zRwBCBxoAgps7ke50dgYhxO50u1wIt3ufwBISeLz07y%2BPwI2yYCiU9WBpz2ADosH9gDi0RiCG8Pt8BDsAGqBbBhLG7AjoHHJFjMhTuYDATATKmVIjEQKMYAEBAAMRIzni7gg7gAHKRttDaICcchpfMcUcxWSvuSqEDlABZT7HY4AfQAStgACpyS1hc02gCaymwEBtIG2BAAnvFMPNvX7MIcAOyIz7bKPbZHbY2mi00ukMs6%2B/0ASQY/A9moAbvySDj6ECAPTbHy6yPRvBUbYQeNm81J%2BlHY6t7YaQNnNBXOg4YjEEgQXFUFgENWoK52NjKYghCFPBsW612h1O13YbbESqYPD5/ZpkMHcPH46KljuHYIJj51HbfO2EjbYsihDbVC1pgo%2BholEV%2B6Klq4Y2qe8zwhG0bbDWdapsGmbZjaeYFsQRaMIcPgAKzltsYBgO2nbnD28R9tgA5DiOY4TlOlwqHOgiQj4S5Wra9qOi6bpbjue6YAewZhu8oZntsF5XjeIZMPeyHPsKoreteBBgBwCh3heKp4PE9DvrW/4%2BIBx6vCBglgZWkHbgQKxAmcfKPsQ9Ymo2zaKribC1DimYQiyFihOgipnJEhBCHgpiYAA8lQECwRmWaoDmOIPgKaoIHQ6CBgAVOWxkIuSp7ZUilIogAiim1kChAXAAGyKngFXgdsJZlkxK6seuboRSVQ56LpUE1YGrZHJuVnIWVlVQZ1Lx6l8mCqOC2wGve8QsEw6DoAA7ug5qiAwAh4KItBBTx5pTRCTBegViqRF6Zw2sGYUQEwYFxvZy4sWu7HuldN3hfdvUnl8pmYOZxBArmC1Lat62bdtu37egd3neB2WCblnxTTNc0g4ty1rea850ckyCHaox2nedl3Xf6t3fY9CbMaubEbhF5OhV9D36eS/2A8DoNY%2BtuMhPjcNKgjepIxNKPTSQKLo/E6DxCt6IQ8YUNiDD5q5ngTA44IeM7RAyBeo1L3061BWLKiJNKmTn13Q9ht0y171M5TrO/VWUZmRZ82Y%2BDWsEDryCC5EvU%2BO8mExpWOVi6jkuzVzsvywoitbQwO0q8F63q5rkMp9D6d6wbT2081b0QKbiondsZ2W%2BcTss4GdvFwzH0U3X/Hs9GHtA17YPY9nqd7XnTDw%2BhodYcgEei58%2Bpc97vdKznac8XdXrGD6h6kzX1tU4NNkRYe8Exc3zM20hNmoaW5ZOUyOIuUwbn0Z5wDeYqPjbOlfkBftt2RZgB%2BM1vYE4rIUSslMCP0ILRljMgFMaI2pDR/n/I%2BztT4JWLFfZkt974eWSE/HiL837nH8sIL%2B4UEHRX/i3E%2BQCz7ICSrQFKYFy5pSVCZSB%2BVtg%2BRjOwo8IJcSsmZFgecMQjBrAgF1ZAws3acU5hwkedwsKYAnm2MW9VthhFQCiUUIZog7CYO4IgABaBQ0MTBQSuPQNgggvypHOvoqCCklJ3gmN0bY6ZtjAHcDyBQOIfHT27jzX2/tl6ogYGvYMG8kGtwbq9JutcT5tz%2BmwrYKJjiYBoCnDoyl2wTGIO4WwCSpGQSOjEIEc0zhPFoLQXMLAcSqDlOVFkSgfA4m5syNaTw4G7wVN1cqZsd6lW6dVXpXZ2q2WkKNZ4rDIJRmKV3MpFSqk1LqQ0m8qhmmtJxO0%2B4nTSoVSqjVXyozhr7OGecI5gyxpTOmbM0plkFnVNqfUokuZVAYS4Os2eeIcRvJ0jsjqXUhl9KOZ1E5D1%2BlDj2RMyRkEcpZTFpBWMVJirwMWqoCA3TnJfgQGqXctBlCoBWjEMKNoVoxXcJCsh2ZIlUPioWYsgZRAEFodsdwDBtxMFoUwSI9AzaAojokqM0EIrJGCt/OJVN%2BrbGbOA9u0yuHJO2NeWgVAuAMgxj3Xm2t%2Ba6z4ZcZkR1iAcuOoqDQipmwlmeOgzybIcQGqNRASIJqzW0jCBazKEC5WxnoFQIg%2BZiApn3uQ6l31qGoLQgYqVLqLVXIRewpVVBX7tnVQEvmWwdXX34baomhrbCC3NV1b1vqYhm11Tau1ubHWRrpBaxUhbUB%2BvdbK6ZncgTxCWgAdUIAgOQDAsDpJ4goOyNMmoxNasGxhjJ2R6pxNoVAIQIDxv8IqsQCbG0CrDMoqe66W2Tutfq7N9qzg0DaLDVJ6SUiUicitQgzKIBUhleuyC4LbKDJqliSV9yllPPRJgD5YNNnoCeKQJtT6jmQsBe%2BvQm5P2PJWS8v9y0ANAZA9GZ9EAQU9N6icD99xKkPOWc8157yWmfOZD85Dj7oxtCUJBgaRESJkVshRccRFpy0XnAxYAGigz%2BjbkjAC2wgKvCpKBRYTbQIQDbegTtooe19umOgQdVImGKikzJ7tva0kKaU/DM2prziBuzNE42jsAGANpShelSjkZ%2BLU12uTWnvKDs9DxzAipcxiE8SvUJh5AxCrOCK4%2B1KPMeADFhgagXbqIS7HEkLnjAzUZDDaApsaFVSfwCYc0xYA1wXIYhUNdLw0Gdy1S2LnmwsFcs4wK5O6hXpZCMALLaEjjOHwoGOLIZEu7szbO%2BdHXfKwLQ/VzLaDitRQQigwstDQG%2BQUBpL8spNP9oYZIyOnwOCLFoJwDCvBvAcC0KQVAnAABaFhdjLFWEeHweg9C8AIJoDbiwECYCWgMCAiwADWIAMINIAJy/a4KGX7oY9Byh8HKUMGhyqSGkFtjgkheAsAkBoU1e2DtHY4LwBQIBTX3f2xt0gcBYBIHo/QMgFA9aTmImTkApg/aso%2B3wAEMRscOoe6QfyzBiA%2Bk4DwDnIQ6g%2BhCpEbQ/Jee8B7JYggIUGC0B5/j0g%2BJOTODELQbH3BeBYEWiYcQCv8DbiqPmdXB2pqVH0esPnQi4cHb2pEQ13PXBYHZ37S44vSB%2BsiEkTAqTtfAD2iYB7iwqBGGAAoKkeBMArRCv6PbfP%2BCCBEGIdgUgZCCEUCodQCvdC6SMAH8wlhbfY8gIsVA8RMmcAMSFbYlo2gvaUGKX82wDHDG5CiZZ5pocGP9%2B4VQTeWDqncK2UwDA/U7cO36ucWAi/vdaO0VIjhe2jG8P4II0wiglAkKQHIF60huCaJv7fHRejr7mLP/k1RJhL83xUKoAguj1GP/0UoQxuhX/8M4h/a%2Bn8SEWAoC7ax9BNtttdt2cMdth89vRckGAPs6xcBCAnw7gbt5g7tA8nsXssBYgZ8vtJA9AcQUcwcNAfBypAd3lfs9AftDBOAEdSAkcuAUdSA0deAMcsccdSA8ctBGE4cfAQCFdmC2DUD3cWd59JAgA%3D%3D%3D)
My attempts in getting the compiler to emit `vpmaddwd` via an intrinsic and via canonicalization were both successful:
```zig
export fn vpmaddwd_canonicalized_ext(a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_canonicalized(a, b);
}
export fn vpmaddwd_intrinsic_ext(a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_intrinsic(a, b);
}
```
```asm
vpmaddwd_canonicalized_ext:
vpmaddwd ymm0, ymm1, ymm0
ret
vpmaddwd_intrinsic_ext:
vpmaddwd ymm0, ymm0, ymm1
ret
```
However, when I tried adding a vector of the result type of `vpmaddwd`, I got mixed results. Code:
```zig
export fn vpdpwssd_canonicalized_via_intrinsic(c: PMADD_RETURN_TYPE(Q), a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_intrinsic(a, b) +% c;
}
export fn vpdpwssd_canonicalized_via_canonicalized(c: PMADD_RETURN_TYPE(Q), a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_canonicalized(a, b) +% c;
}
```
Emit:
```asm
vpdpwssd_canonicalized_via_intrinsic:
{vex} vpdpwssd ymm0, ymm1, ymm2
ret
vpdpwssd_canonicalized_via_canonicalized:
vpmovsxwd zmm1, ymm1
vpmovsxwd zmm2, ymm2
vpmulld zmm1, zmm2, zmm1
vpmovqd ymm2, zmm1
vextracti64x4 ymm3, zmm1, 1
vshufps ymm1, ymm1, ymm3, 221
vpermpd ymm1, ymm1, 216
vpaddd ymm0, ymm1, ymm0
vpaddd ymm0, ymm0, ymm2
ret
```
Full code:
```zig
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const VLEN = std.simd.suggestVectorLengthForCpu(u8, builtin.cpu).?;
fn PMADD_RETURN_TYPE(T: type) type {
const PMADD_VLEN = @typeInfo(T).vector.len / 2;
if (PMADD_VLEN == 0) @compileError(std.fmt.comptimePrint("PMADD_RETURN_TYPE received type {}, must have a vector length of at least 2", .{T}));
if (@typeInfo(T).vector.len % 2 != 0) @compileError(std.fmt.comptimePrint("PMADD_RETURN_TYPE received type {}, must have a vector length that's a multiple of 2", .{T}));
return @Vector(PMADD_VLEN, std.meta.Int(.signed, @bitSizeOf(@typeInfo(T).vector.child) * 2));
}
const Q = @Vector(16, i16); // PMADD_RETURN_TYPE(@Vector(32, i16)) => @Vector(16, i32)
export fn vpmaddwd_canonicalized_ext(a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_canonicalized(a, b);
}
export fn vpmaddwd_intrinsic_ext(a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_intrinsic(a, b);
}
export fn vpdpwssd_canonicalized_via_intrinsic(c: PMADD_RETURN_TYPE(Q), a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_intrinsic(a, b) +% c;
}
export fn vpdpwssd_canonicalized_via_canonicalized(c: PMADD_RETURN_TYPE(Q), a: Q, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
return vpmaddwd_canonicalized(a, b) +% c;
}
fn vpmaddwd_canonicalized(a: anytype, b: @TypeOf(a)) @Vector(@typeInfo(@TypeOf(a)).vector.len / 2, std.meta.Int(.signed, 2 * @bitSizeOf(@typeInfo(@TypeOf(a)).vector.child))) {
const c = @as(@Vector(@typeInfo(@TypeOf(a)).vector.len, std.meta.Int(.signed, 2 * @bitSizeOf(@typeInfo(@TypeOf(a)).vector.child))), a) * b;
const d, const e = std.simd.deinterlace(2, c);
return d +% e;
}
// Not the best auto-scaling implementation, but it's a start I guess...
fn vpmaddwd_intrinsic(a: anytype, b: @TypeOf(a)) PMADD_RETURN_TYPE(@TypeOf(a)) {
const Definitions = struct {
extern fn @"llvm.x86.sse2.pmadd.wd"(@Vector(8, i16), @Vector(8, i16)) @Vector(4, i32);
extern fn @"llvm.x86.avx2.pmadd.wd"(@Vector(16, i16), @Vector(16, i16)) @Vector(8, i32);
extern fn @"llvm.x86.avx512.pmaddw.d.512"(@Vector(32, i16), @Vector(32, i16)) @Vector(16, i32);
};
const V = @Vector(@max(8, std.math.ceilPowerOfTwo(u16, @typeInfo(@TypeOf(a)).vector.len) catch unreachable), i16);
if (@sizeOf(@TypeOf(a)) > VLEN) {
const half1 = vpmaddwd_intrinsic(std.simd.extract(a, 0, VLEN/2), std.simd.extract(b, 0, VLEN/2));
const leftover = @typeInfo(@TypeOf(a)).vector.len - VLEN/2;
const half2 = vpmaddwd_intrinsic(std.simd.extract(a, VLEN/2, leftover), std.simd.extract(b, VLEN/2, leftover));
return padWithUndefineds(PMADD_RETURN_TYPE(@TypeOf(a)), std.simd.join(half1, half2));
}
return std.simd.extract(@field(Definitions, switch (V) {
@Vector(8, i16) => "llvm.x86.sse2.pmadd.wd",
@Vector(16, i16) => "llvm.x86.avx2.pmadd.wd",
@Vector(32, i16) => "llvm.x86.avx512.pmaddw.d.512",
else => @compileError(std.fmt.comptimePrint("got type {}", .{V})),
})(padWithUndefineds(V, a), padWithUndefineds(V, b)), 0, @typeInfo(PMADD_RETURN_TYPE(@TypeOf(a))).vector.len);
}
fn padWithUndefineds(T: type, value: anytype) if (@sizeOf(@TypeOf(value)) > @sizeOf(T)) @TypeOf(value) else T {
const padding_len = @typeInfo(T).vector.len - @typeInfo(@TypeOf(value)).vector.len;
return if (padding_len <= 0) value else std.simd.join(value, @as(@Vector(padding_len, @typeInfo(T).vector.child), @splat(undefined)));
}
```
Optimized LLVM:
```llvm
define dso_local <8 x i32> @vpmaddwd_canonicalized_ext(<16 x i16> %0, <16 x i16> %1) local_unnamed_addr {
Entry:
%2 = sext <16 x i16> %0 to <16 x i32>
%3 = sext <16 x i16> %1 to <16 x i32>
%4 = mul nsw <16 x i32> %3, %2
%5 = shufflevector <16 x i32> %4, <16 x i32> poison, <8 x i32> <i32 0, i32 2, i32 4, i32 6, i32 8, i32 10, i32 12, i32 14>
%6 = shufflevector <16 x i32> %4, <16 x i32> poison, <8 x i32> <i32 1, i32 3, i32 5, i32 7, i32 9, i32 11, i32 13, i32 15>
%7 = add <8 x i32> %5, %6
ret <8 x i32> %7
}
define dso_local <8 x i32> @vpmaddwd_intrinsic_ext(<16 x i16> %0, <16 x i16> %1) local_unnamed_addr {
Entry:
%2 = tail call <8 x i32> @llvm.x86.avx2.pmadd.wd(<16 x i16> %0, <16 x i16> %1)
ret <8 x i32> %2
}
declare <8 x i32> @llvm.x86.avx2.pmadd.wd(<16 x i16>, <16 x i16>) #1
define dso_local <8 x i32> @vpdpwssd_canonicalized_via_intrinsic(<8 x i32> %0, <16 x i16> %1, <16 x i16> %2) local_unnamed_addr {
Entry:
%3 = tail call <8 x i32> @llvm.x86.avx2.pmadd.wd(<16 x i16> %1, <16 x i16> %2)
%4 = add <8 x i32> %3, %0
ret <8 x i32> %4
}
define dso_local <8 x i32> @vpdpwssd_canonicalized_via_canonicalized(<8 x i32> %0, <16 x i16> %1, <16 x i16> %2) local_unnamed_addr {
Entry:
%3 = sext <16 x i16> %1 to <16 x i32>
%4 = sext <16 x i16> %2 to <16 x i32>
%5 = mul nsw <16 x i32> %4, %3
%6 = shufflevector <16 x i32> %5, <16 x i32> poison, <8 x i32> <i32 0, i32 2, i32 4, i32 6, i32 8, i32 10, i32 12, i32 14>
%7 = shufflevector <16 x i32> %5, <16 x i32> poison, <8 x i32> <i32 1, i32 3, i32 5, i32 7, i32 9, i32 11, i32 13, i32 15>
%8 = add <8 x i32> %7, %0
%9 = add <8 x i32> %8, %6
ret <8 x i32> %9
}
```
</pre>
<img width="1px" height="1px" alt="" src="http://email.email.llvm.org/o/eJzse1uTokrT7q9hbojVAcX5Yi6Kk6Ii4hlvVnAoAeUkFKL--h1A20e7Z-bd79or9hdfx8RIF1lZT2ZlPplV3e1WVRxmCP0kOJng1B9ujaO8_Ll2kzhwy-MPLw-u7btBHnh5gskkzo4EpxJAjDAuKoKBBNAJoN_i8CnsZZ7yMuwGmRvBQMuZhBHU5VPA2coFOs3MkU0lleeBPIE6bSs-dGeaAs1bI2MTCtBudGza8hXOQ2mMbUeD1yr2KHsLlYsoy3Ccy9kSwhUcFg00z1B3QngbrwtzkEOd48JitN_YmgivOWPL0XpiK_VEjqFsbldHFppZaKZQvmahPEhNeyGH0DrBkyaF13qY8ATggEwAPc9GQTiR87lWUZ7uy9h18ClRZO2myVaKmIzbpOqOU9U1LcNttFrql3W5pQ-7I1YqUzK2jA7BCia6mwvV0hDkS67KJqTlWQ4a5YhCGIMGKki5jqQ5HM4OnBDchNu8kRX5ksOwqIQj4qggdKKLxVE13RiYqfeNbCzQ5EYJ1w7orDHA1VHilbSRixuAal4NpVCNqXmsDMQZFmW1goOT7MnRZCDAQz4cjvSDGS1rR4VLuByna-Og2WPTgZoMoTk3blkshKEQwuF4jrHqGorvR8Xe9Ff-ZbM414uTpiZQ3i2FocPCvQ0XOIRKcV3QkU-BUHGvaaPJ2BgAGcLsrJuz6XkrmIYPratxE7zxzNsNPQekMUXVR7N6cTgFF7ZDAB1KaQ6983WhHnhNgbOlN7zy19n8LK88x0bTVKRH3Zxk6PBYcFx7mk3lrRWyzkHTQnurr_RQm27yMQemh7ETRoo-k6sNT4OItmjG8TxtcXJmKkcAfVcczaE_G7L8ODnUQ8cYHTh66O0toB08p4oj1VsrdBHm3YpRnmjr7okfJyfWASck3ppoQHmeugncohqk3u7gIkev7LlrGv4shONbgitHDi2QeZkKxLmEQ8ipseWwy8RKmtPQCmP7wC64TelKJyvmvcI71Ih3IGp2US3muwxXOrc9IGNnztB1OKvGeYkwkuVQbpSIidC4YGeapJ7LDQF0tgNoyBf-rMhhEu42x6EX2uMLTP39eFbZkpPm4OaOy9hTaDVOHTu-6nFkhXGRjbRIVoZhETsQiomtKc083bi5mlrGLBy7uTG0eUUO4draXI5GvSiE3SpOjDyEcZzSRyWCWwgH13EW6tswqEa75HgZ8xWqvXy-WNscPylWGu0Myt1wVK8zY7NO1vaOnlz4REyVk6WAZnBGol1pDa_mEAp1yJ1i2MCIHRtYlpcXrljOlaWMwnV8muv8rgHVwFnZWuPol_PuylabWhjWSTBlon1wXmWJWKSOtbvIsEwVeuJiHngBrhkBhQGTIRlsFC_JLsvl1KdCFl_pErkiR_nHqzJ08wvIUJAw2z2Pmb0jKZ7p1p7mRTpnRWtJZXi0IIAOoKjmU6dzPK7NaHbwp-PwgArjMh3Plgt7PMkJoGu8elEwb9iis5rGqsqduCxcQpY1kCnM4VVGOT1hKJ6WN5NxPJqO02VGM7YUYepKC7TJLUuLAPp6s9ql850I-gXVTaWp5VVROO286kYsMdUuxcZax_zhWN_Wt-p8bfIYe3ZjUSdR55kTVx7NQlL5gG-8JaBE2qPssRfnWVDtwKbT4i-rih0EZ725DakVsyWAPqykwRBDWnBmjpSdAmGbLdXmOTUCxw81FHNi933hS8I5GN8y1xe3wbYcqHzEH29bGq_Y6fQ4d5CoXXzmZgIqmqa5u-F21upqGfWq8TJqxzUqck6zg2cl-6O8xxjMd6HcaTbps6OxpbFaVImpzChjMk8OZqibrgEKdU4APSOAbnSyN7xkR5uJvt8dsSSwaLPQWjPiicgI2VCsjEknxq604bw1MGa0bsBWboKhLEpbHzv6lSsWa2W36Jdf47D7nB-sJheNkb-o0p0eIt-0D3IJQ53ewaZiXH_FRc0RBU01dmCWAnMjrXEU6bYSr1bGLg8FxbFYI4SyK9uUuZTtRT2zp2EYnq_F0DrnJ0U5JiMmTKeU5_A7fIBDX73KtmXsh0sgHG5bQAAdJ3weOoOzYw7E4wA33kka1dhfcvjWwUyb0hmPDrEj50qm5tcENMuBYbGbRlkUA3thHTRD86lbNNPzib01J9BfnNTEWuvHRbiHbMpV16m7bNiB0KyntH_aVQGfbPhgvS22O3ACm-N4umNL1RjN0_GaMQt9cTrSk5NNpeIpSlM8ulhU7-VsucjXi9Nhvb3mt6gUrAHyDS68XoqllXomVSRHPVtPT_tVfqx3a-gsWKsCCBn-_DyiDtXhtF1FiTS2drTGqXRfBNQDmJvX-VHzi91SL-SNuR7FZWNuTrkCmWU22of24LT1sZwoubw5aOZ4mq_zy9YfKVUEdkaymsX5xqnC3Rxqeh5UQaPK-GLq-apZGBK8uTA_5MeTSWlGORoomAC6NVpPu-VLb-BZUKqpaz0cWWCKNuEa1tAMLwdmmwLcCbn8jd35u-OcjqFfW_lJteMibZIUWeHt6hm-PThN53BhjE63DXVQZPc21hRuc8ly3Tb6cNvFsMiY2uYThj4N8okdOqf15DRZb405aor1Wt4KOFvb2jndNMF2y7Cem6dR6SkKM13THn0-OVFK00u3pmtvuJmXhexeVpG36sP6HHhjfpf54fKibEIYrAwYWrYKK7UYsWFuXadmml-mbjW4RIcRmPPFZZqvoWEb6uUsGqfKVrwBpZSqmZ8Qz2_Assb7kzCVTreQUt2Juh7xi1W8iv0Y2-FtbMirKEH1VWEn3lgdnZJ4sZauSTKbzUehyjnTaZYraGyP8JI_pHYlox6mnXIzMBttFuJZulJwCndQGuwkDdoracrHvipJl_FxZm18keaE6ybdWztV5NSDkF-wshrJoTvXFqPjuormvj_Xiyu1zeDFgYM47DPm4B0PUAFNOD4rxVieO0vPNmzVQ-ES5zmipTq1QjtYG-lgHB9vIwGfXeo4dlcE0FUTFOUtluvbVGPBoYIDOC0WEUoWh-Ou0z5Z0aBebrLzWK0k9XKDceFfYk1cnFejiIMXpbSUcV_C6fHMRFDVo9ANt97JWAhD7TjJDvC22qmqC4vK1deLfaM4PnVxIN1sDSdaTYNJQw2CK70AkyxtKt2v2GbMWXy0LXaKCScTVzscrGGTD6yJsdNqobp5VrdgMNzux83EbmvMbeKqMZ7bdhw2FVbtQc-LK7mYiFUxnUm5szOuVtZYykTPR42qnOUz9BVqbK9hBqHs6lFwmOgbDQ0bFEqKCI1RrnpxoymOsglNBcYNVDcGNI2pAUdXAuguOx5shdyR5GskDscrcxBdT4vBUceH8BghWMGjnK9oBFQ8dNhJI9uGvNwye3DBVOjLjmwM2zJrm6EOJXeZGunyCIv2JOJWfQPHNFAz55Vv5Yi399ebHMNu_MiqjZoZNt8nW6QUmte6YYSQKAsjJwyN1UCh5LZlPFC90MhaWqo2sb0hIyPZ0Tax49vKWVS9-DRLg60MitN6SKFqltkx6w89UGg2zcy2sjPcMcKCZc_PJaWKteMSnpZ4D1UQO7IQNyd5MIAwHx-RbMJyrpz5mbefddKKImsDIwhX4U5RtJWSB7ZyDhR-YcKh2CSRt3fE0KjWUJybqQ_Nhe45SQ7Cs7sajLciaNShOuCUeLfjitwfzKSenNf2Em3UU-aPwapR2zPdSvAthq_9DYwJoJ8D16LWxiFC4DAQZ6xhUE0cJvA4toeGzlNRrbijs7AfztGhFNLJjAD6kZ6PoonIXFbjHIb1gR60o9Qqty911NZb0WSjdns6CJm40DYwVwT3IslTjHGAgW8GOBKl89w_DuCs4i--rMCsEUIPc6GAoZhV26qSnXAnnvEIStC3V37jT-FevhYwYJJ9JcE9VmULasECHv0aroIFpAIETb_y_WABRR_LA43199BW9CBVgLpSGX8TcNIIhm14MOrb_ySCggQFzSvpYozSAldknJEhwjjOQhJHiPTztIgTVJI4J1EaY5LgqXORukHQBARPkefYJd2MjDNcxlkV-6SbBd2g72Z5FvtuEt9cHOcZ2aASkV6OI7KqfR9V1b5O2vN2h4Dgqf7fLQ4JCqJLkZeY3Gfkfa2_3-hDwd_oggkgugQDSZsACum1TwRLLa8FsvbdKyARQCJnJlTVv-facjWf_r10ZhoBxEdyhCATFCRJkiwRrsuvFu5ntOsBiWDaKYSg9iY8wPzilX8M70ewLyt-CfTu6A9-d6uUoOA33u52inz-usuR1zSl2mWuaUo_f1Jv5EqE-3W-cMkvlb4q_6T0vSHDvEFnVLbiTYQy0iBxGaOAdIOgDWWXPCMf5yWZ77uwLlFVJ5jE1wK1Q--DulVikGGOyTS-oOBZuHoilTxAvxOyQdFU1UcnnmP33fb4bQg82m6722KF_JeihSRAy-Sk_018f2ngx2T5F4z8kxR-bOv7yNLSGH_e9Hu-_MZW90FOCPIZXQhB7SK9n9bH84McAs-7dU-e3_T3x3TKz9WlCcjbq2L6oUD_dWvXfbP-q1idJK9a7mK3B9pOQT_7wWt0waXr45hnL2xvNPMiBhSykz1XUb0vqreeuH92wgC8XxGVaRF8kgY03ykr3CAIHrqXeqflkxj1YBfex4ReJwnpf8MGfp5VmKxwQBKM2kZ1nLbZ00YuABUOCABeiLmX9eo4wXH2SP751ec5blWhEndTKhw8Bcirw6d-8K3YeqJNX4SqOA2eqjoMUYXXHSdOUBbiSM9LpagJINZilx39mk9-OyY9EYz-nCIU3GcPU3LZ5m9LqG1edcT6mpA9jn7WCxqCpVoxI9vn3XQgPfUk_ZSgjCSAToJ-zVZDvCcJIL7X0Cqhuixmqec-RSvLvCSA2Fq6T_FTO4zjFM3KOHv25ifsZIl8FJ9R8IpaUFsfpHWFycg9o9fykXS-akuGi8kEuRUmQbcvCvlECPKymym9bNMr8l8Zy5GAJAD975mEIxcTQKhIl0zrBMdF0lXGX1n3TLUES_XB9G6T2okt7BRh98no0D51P58J2jcES3kxXsS3nsa_9pAfxUnQszUkwVsEL8WpDzD7HlcvWGi-XSluP9opZP_Tna9Kyss0BryZ1i7cBpv2SDMDXrro_21a_7Gm9f-P9urBPvyP7LD-L9urroJ8M5mBpJtd-0ryDei3yfieOx5If64s31IT6KjmW4L6ZpE7X32Kjp6l_DtLudUH2vkTM_7fGdAH0TP9eq_U31vTLdc_ovdNRoDiDKMycX1EALHzuP-odgT3MEEfwuSZrKc57s5sHmpbnhrnf1VtuGQhGadFglKU4e6GoW9bMBnf61iF3RK3J7kaVdXT09OHyHufm78ZdX9OCb1vVLSPs7iFWT17qax9_Eas_UIXjMqsJQCCpQgAkuScPl1E_qmqEHjqcD81fe_4Lm7Et9VKIb989T5p2DcFjPktGO758h2Md8X2A47376QHGP8QCEc_Q2megieOBp_hvC_i7-F8KvBfVfYXPG1U3jvg121df-o4CJZK3cvdqi5DXRw9-ShOZnmDSmu_bNr0q_tl_izlJdJ3sR-RdVYi149cL0HPxr20OK8IXxrP6g0BfA5URiP7Xk36EI29hZGb7OnOyoeJ85Ltz-e7O_13Z6herw6eMT6Q9R7LfoiDHkmC9jg_o_LB4eEXjP_Xq_YHelsLwZ9b-ApYeYH2C0O_nPLB3mdiLNxgE-NolQUteaCgemmuf8k_71Ac8vbwKHY72b7oDP647gvrvgHwwA6CpfYxaquD-IbSuuWauA1NAojrz7H0BSW9NNbfcp3S3Z98QSaPVHzmKeULLG954AtND4nmrT6UVOjNCeH3j21hjt-fz15OWuvXk9az9c_fi49iYn2v0O3nlwLea2RQn5nnt-PqAyF9LNldiX2E4c31gEKe3aRG70qu9Au66me8UtZbueUriX-S73dn-akeF_3F8N9dQ_jLy4i_vuGbN8jeuuZjl9Ob935Z5eW43ynpsX5M22f9ysOO8Y2-z3v66Az9LFUVSXvcF-v7Fr12e1_fhFptALdtOjmZrM3PV19t1hAU7DWSQZX_neS-m7SGiuSlq6j9zn17PiYYheZbcZrvs5Hrw_XTMN06rlvh7zrL3BQFf7tBUD5vtZbh8nq_ESUA1zN8hS74gSqKxPnrcIfzPo_5bh799Ty2m5fWCZlVzQeZTm9nFODAfQLXLxTV-32Cnu9mPk9j3_miHy7yuOrb3w-eZpSYAX22tw_g_nDv_Mh7p0PeWzCSfpGmX8Rp9o1d_D8Fk74vx9wfuPuDcH-QXiC9SNMv4jT3BqbQwXSD4ONigOOePd_dErfp-VlEeMtpfxDPH-9O_tFYxm6ckL6bPAD0VSX8M0Bf-we894-fuCX6j1F8BtAfNhn69_3_W1dCn8z42vZHw-CP9oj5L-7RN4C65uDONg_D_U401Nfbyf5puP_27dS_4PL_lKq_mAe-nsf9guLZZ88zf8ad3L9N8cI_BfO_S_Hi1zEvvIt5AnDS17Lir8uB9Lkj-hH8ZAKJkdwf6CctMAygOInhf0Q_kei6Hs-Lgi96tCSBACFRlAKXkgJEu9z-R_wTUIClAcVQPMUx_BPw2X3gShQtuLREuy7BUih14-SpY4i8DH_EVVWjnzQtsgzzI3E9lFTdH7gAkKGG7N62hwdO_VH-bCf95dVh1VJMXOHqVQ2OcdL9ZQxcb_9qTzKcSppxVaHg42_1uNnzUJ_o7dDH3-f5UZfJz_d_NBPGOKq99qRDAL1rBvuPv4oyP6D2-Kh3WCsC6M_GnH-C_xMAAP__g7RIyA">