From 49cddd2db640d72d8e5a5110491ed370ccc94f26 Mon Sep 17 00:00:00 2001 From: Run Wang <52746141+SamanthaWangdl@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:27:58 +0000 Subject: [PATCH 01/79] Deeploy Microbenchmark with GVSoC CSR and Demo on GEMM --- TargetLibraries/PULPOpen/src/Gemm.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/TargetLibraries/PULPOpen/src/Gemm.c b/TargetLibraries/PULPOpen/src/Gemm.c index a46f8ac6ae..02fd991674 100644 --- a/TargetLibraries/PULPOpen/src/Gemm.c +++ b/TargetLibraries/PULPOpen/src/Gemm.c @@ -6,6 +6,7 @@ #include "DeeployPULPMath.h" #include "pmsis.h" +// #include "perf_utils.h" void PULP_Gemm_fp32_fp32_fp32_fp32(const float32_t *__restrict__ pSrcA, const float32_t *__restrict__ pSrcB, @@ -17,6 +18,16 @@ void PULP_Gemm_fp32_fp32_fp32_fp32(const float32_t *__restrict__ pSrcA, int8_t core_id = pi_core_id(); int8_t log2Core = LOG2(NUM_CORES); + //RW: Performance monitoring is currently disabled + // perf_stats_t perf_start, perf_end, perf_total; + + // // Initialize and start performance counters (only core 0) + // if (core_id == 0) { + // perf_bench_init(); + // perf_bench_start(); + // perf_bench_read(&perf_start); + // } + uint32_t M_chunk = (M >> log2Core) + ((M & (NUM_CORES - 1)) != 0); uint32_t M_start = MIN(core_id * M_chunk, M); uint32_t M_end = MIN(M_start + M_chunk, M); @@ -351,4 +362,16 @@ void PULP_Gemm_fp32_fp32_fp32_fp32(const float32_t *__restrict__ pSrcA, } } } + + // RW: Stop performance counters and print results (only core 0) + // if (core_id == 0) { + // perf_bench_stop(); + // perf_bench_read(&perf_end); + // perf_bench_diff(&perf_total, &perf_end, &perf_start); + + // char label[100]; + // snprintf(label, sizeof(label), "GEMM M=%u N=%u O=%u transA=%u transB=%u", + // M, N, O, transA, transB); + // perf_bench_print(label, &perf_total); + // } } \ No newline at end of file From b260e4e7bcdacebeb5a97297310cb338a2e0caaf Mon Sep 17 00:00:00 2001 From: Run Wang <52746141+SamanthaWangdl@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:55:23 +0000 Subject: [PATCH 02/79] Add float concat and Change padding pattern of ConV --- Deeploy/Targets/PULPOpen/Bindings.py | 3 +++ Deeploy/Targets/PULPOpen/Parsers.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Deeploy/Targets/PULPOpen/Bindings.py b/Deeploy/Targets/PULPOpen/Bindings.py index e1a9ed5932..90d23264e3 100644 --- a/Deeploy/Targets/PULPOpen/Bindings.py +++ b/Deeploy/Targets/PULPOpen/Bindings.py @@ -368,6 +368,9 @@ PULPConcatBindings = [ NodeBinding(ConcatChecker([PointerClass(type), PointerClass(type)], [PointerClass(type)]), ConcatTemplate.referenceTemplate, ClusterTransformer) for type in IntegerDataTypes +] + [ + NodeBinding(ConcatChecker([PointerClass(float_type), PointerClass(float_type)], [PointerClass(float_type)]), + ConcatTemplate.referenceTemplate, ClusterTransformer) for float_type in FloatDataTypes ] PULPiRMSNormBindings = [ diff --git a/Deeploy/Targets/PULPOpen/Parsers.py b/Deeploy/Targets/PULPOpen/Parsers.py index 5c5951eaba..99e45cefb3 100644 --- a/Deeploy/Targets/PULPOpen/Parsers.py +++ b/Deeploy/Targets/PULPOpen/Parsers.py @@ -75,10 +75,10 @@ def parseNode(self, node: gs.Node) -> (bool): # Current PULP kernel only supports grouping of 1 self.operatorRepresentation['group'] == 1, - # Make sure padding is square - self.operatorRepresentation['pads'][0] == self.operatorRepresentation['pads'][2], - self.operatorRepresentation['pads'][1] == self.operatorRepresentation['pads'][3], - self.operatorRepresentation['pads'][0] == self.operatorRepresentation['pads'][1], + # Make sure padding is symmetric (left==right, top==bottom) + # but top/bottom can differ from left/right + self.operatorRepresentation['pads'][0] == self.operatorRepresentation['pads'][2], # top == bottom + self.operatorRepresentation['pads'][1] == self.operatorRepresentation['pads'][3], # left == right # Check number of inputs # 2 inputs if no bias, 3 if layer has bias @@ -133,10 +133,10 @@ def parseNode(self, node: gs.Node) -> (bool): if wellFormed: # Check if the node is a depthwise convolution ret = all([ - # Make sure padding is square - self.operatorRepresentation['pads'][0] == self.operatorRepresentation['pads'][2], - self.operatorRepresentation['pads'][1] == self.operatorRepresentation['pads'][3], - self.operatorRepresentation['pads'][0] == self.operatorRepresentation['pads'][1], + # Make sure padding is symmetric (left==right, top==bottom) + # but top/bottom can differ from left/right + self.operatorRepresentation['pads'][0] == self.operatorRepresentation['pads'][2], # top == bottom + self.operatorRepresentation['pads'][1] == self.operatorRepresentation['pads'][3], # left == right # Check number of inputs # 2 inputs if no bias, 3 if layer has bias From 5a79c7970080556b25ab3e89678046563541f0ab Mon Sep 17 00:00:00 2001 From: Run Wang <52746141+SamanthaWangdl@users.noreply.github.com> Date: Sun, 15 Feb 2026 00:38:41 +0000 Subject: [PATCH 03/79] Support SleepViT on Gap9 --- Deeploy/Targets/GAP9/Bindings.py | 3 +++ .../Tests/Models/SleepConVit/inputs.npz | Bin 0 -> 12264 bytes .../Tests/Models/SleepConVit/network.onnx | Bin 0 -> 118943 bytes .../Tests/Models/SleepConVit/outputs.npz | Bin 0 -> 286 bytes 4 files changed, 3 insertions(+) create mode 100644 DeeployTest/Tests/Models/SleepConVit/inputs.npz create mode 100644 DeeployTest/Tests/Models/SleepConVit/network.onnx create mode 100644 DeeployTest/Tests/Models/SleepConVit/outputs.npz diff --git a/Deeploy/Targets/GAP9/Bindings.py b/Deeploy/Targets/GAP9/Bindings.py index 0e7b052f46..22c87cdeb0 100644 --- a/Deeploy/Targets/GAP9/Bindings.py +++ b/Deeploy/Targets/GAP9/Bindings.py @@ -328,6 +328,9 @@ GAP9ConcatBindings = [ NodeBinding(ConcatChecker([PointerClass(type), PointerClass(type)], [PointerClass(type)]), ConcatTemplate.referenceTemplate, GAP9ClusterTransformer) for type in IntegerDataTypes +] + [ + NodeBinding(ConcatChecker([PointerClass(float32_t), PointerClass(float32_t)], [PointerClass(float32_t)]), + ConcatTemplate.referenceTemplate, GAP9ClusterTransformer) ] GAP9iRMSNormBindings = [ diff --git a/DeeployTest/Tests/Models/SleepConVit/inputs.npz b/DeeployTest/Tests/Models/SleepConVit/inputs.npz new file mode 100644 index 0000000000000000000000000000000000000000..ee174fcab40de795ae92ac965156ad8b3aec9ac2 GIT binary patch literal 12264 zcmb8Vc{olNhxg;R$|Ss9C_;DyWgyVd5ynTH zAjV`pY*DtRdRYp%d}ciw)W?DAKONd2-$_S!(qQb{0aU5n4)30(k*w|r{7W)9%F_O@ z^tvfHxgpG7R!k}$?_}5J9K!7j9@5+`{+#UeO5Are5{9;@Hju5JQcb~w9-p}G_EF6yL0HMr3MHe2&aYH>S%>X9(0~a zr3b&Nac>WnlEtC-DT#)E^Tf2KP9GJg-r^5EraxF5sM73J`3Jgv@I=PA}~ib16X z!FV;ZgBcHgK{dFl7&;@=Ks#%cxa3CArHXSf`%yRL|5Hq~yS~>Zdl-R>MhbcaZp6gT z#*k*T6+KjLLgR~asMRZj;$!>J9L>Fq~W z)MVQ&vTBGAO6 z&7LRBW#$T0;IG5toRz11@wn;<*!*KTGnFJp74-jdCZiZox;jYfK5qn*u1Hv|Swfv8 zJ+RD@A8)njK+*vYleR?(h6`WA;*;(qXs$UH?uv#+J1eHl#tv5bpMr!?AB=ui3UjJl z;6(ZYtbdhB%1gsZp;#lUUdIkhe^Km$bxeJF=jWMR+2^?8`jXtTnME8_k zB7S=xVayCq$d$bWlN$BpxZPv8&~+7G|NR6nB@NK^NglqQ>qQ5$he6Rex6Ya##$s*( z+HLCQ4BGy~Tke;Ugx`fq`>Ws|%ullqeWk*MN0?OOx$GY{Z6xFEK@hSur!|ND8Sm>$ zfmeZ-uwRVeouNE%@t4OZ3PNnNE^V0FmCOVUzamT4yyR4iPtd%x)}$`M0eYT(p~IUM zFq3ckpQ`qPSTTw@ai z3BRVB`r4__k4U1zXTmD8JPcQc1yJO-C)kX8)44ge&~f)PZTcaHkDEqe&#qHYxBnfU z#JBY0x(v|JNJmXmV_4Cs4#vM%p`DLJUEdu6Zb|mfi1@<2rrEQ|>s5Jiuc-!%DJAO6;xt~E>!c%K`Z)oAggT|aI6_?<@ zoyOp@eu{|mS1~A3gS%yIaL%m~nr!`%%(|Swd}|7T%_m1lRn9(U&SgpV$W$ANR(V04 z=X`~^h3Sy&GX$Gfyx|-e_J@g|HCRxw37o=|=xNVlWNSo#L)joEKOV)1b2X&PPzu=I z$6@n#2A1)2K*4Y|g!&6nkNR_*Ehm#`*QPY`#3v4S$KN2y6Qvw+#dy+Ekw6B&%W+rM zI%CE0mz-5bj-2905#&H?1{t1n2NEZ9NrBM?DqZ>ze`=QCUl|dosf&gJ)4S06Z5t|_ z&7@Y#j+0bjes&P+6Vsg71oq-uhObs+JJ~tKP29524y&nRx$mb)gbleZp-XF)ZdzTo$nGIlL zAkVc3GRN6TE;!2x=svC?$GbZk`6E?OYQz{XXOz=sA3IXJz!m1oL_>iBW*BgyGKgi7IV}EyvPozIHGN%kJ>wh z@eT;Vg8*yNaF>tmW!?q@y@z2ouOYTYNuZ=*2<~Pjl1GQMxl7wW(qWNoI=Zj{J=bo5 z18vEeEE7Xq)mE{O=sd#b^7CnSPCLzAumC%9H^9oQc#uAA3|tL;T;8~lZJjoND>Nf8 zyj7E^Nmnx;Urmsw>+gfJ_Za8z$026Ts`*%+H5YPb=h0)k^JtaQOxS!p0&*WOxi<&0h=FH+|WJE#I$WAOXzK_0P;jHzayeVI`6^QsaLE)1I zW=psOj(<2qs#VoUSAisK|6zyM|2(9b&V1l@B^Qb|`hkPk6{b6Of+p8LA|++9$fsh7 zE7lzM*7r`VxtT ze>Cnk<3}jfBH4r6!F-kz>NHDprC+QiCM(p~pQ_BMvpSpkCjS-~xp~kjv5zd;J3uBB zLWuvHWIR!E4O7Fv!zWV~>HhnW+0=52b7<5AKKd_aHz*0h^SHUl$`Gb*b7R1Ll?*=C zu!Wyd(%kq1hiFTyBKW6Vg6L>V%(UgA^vOtCE1ras+4HHsKwmBPUcs8-t#D&Xk%nzK z4bPT-M%&e4c+6-9grC`hB7v%O%1CeBGx5dbx3?yqeL0`BSZ~6DYz^4C`V;4K{UUP1 ze+fIiG9D#3H=%u%I#+Rq0N1oN2iY~}a7~&8(Q!UYhW)~5mcJ}bTPOr-X4^sd;ZDkSfI$EE6{%66>hk& z9)7>5z?&(#VA3zjEj{)M1w&?{UCVJ$&H7WG6upX14e?Red+%o)lCK7s=?t}CXNkqT)Iwri4f!!B% zK_QjRyiCdh$1yDwisHjp{0iI-|3eT_c7ePx?#3qB*I46Zhs!lhp|3feMhKeW!R8R$ z6cL4Y!^UwWHUle!WAFf13(U&ZX!Ndac(QX2d)b&8UUW=_z>qfZ9c{*U{%OqIpcyE) zO%^pZXTdRV1-vr+hgvMtpewYH#_65IXs#(3>PEr6uO%ex0hiu9wFW&E*Wmj8xonNO zN5NZiC&pb?B`b4k=yxv}xU+aB)RdnFNxBPtPd;TF+xs#4+XXyg?Ew7tUf6296B{lF z)jfZ<5;R((@yy+Q+#gN*P{1k!oJ&ebt@l;1?N35ZcL%CJ{!Aj9eMru+Eb_AFFw7n7 zCUV7Xq+C=F>~nbFZ0K&#ofiuGXElP$%4Nui{Um~!9ID{79jhZ(V4K??vU=%L@`$gM zoKSM10{86ay2$)~vPWee-;x?4FKsJXm#Tw zr&E40^v=!3eJ7uQ;PV4u6Sa+2{MNxp)8*(fk;X&}xzT0TUJ#uzjg1cS!s{QO>lR3z zCLdD{(OwN5aQw9vIs!hC*-;BY<<2oY9#TMh-sQsn^xq`@YdHSuHs#3uK{EVyJ$5}S z#B>R|W98iHmX$w1c9H~~@L7yGAA+IeZagiWYO9;IVkKoQUx82b7qK-4{?K~9 za1fb_Mcz}C)M|~9hqIdM_GwC?QfCzX8Ssccye-R?SFfp)vlE2PVsF8Hhdt)3U%)hW z#!y?=QqD&+J-TI&0bY{bL~LiA!b2)SV34c;YK2qu*^@W)*6euV=dlIHzYfC84b|lS zt`?&0r3IBcrSXPSEF4ez$kegCK)+%$P06|f9|Bv+sv9%e-&S&{vyUaYDH?(ELWGbw zEP{&oB&fZ^ur4apbBRs#@ zN>n4XQK_YXXp&UA#qcq%JY~ii4pO8|Q_;}FGn=d5bCeW*NI`NekK|oMQvB>Dr=iFU z41IF3r6nG&80lcC$~_G9i=`6t1i3QGVZ^^Kf_i_*hW84~;G0GnCSBNphMNUIqt6CD zoX7%EpG>&8vzqg-xQ0kDmjO&OssEunnMDfA28l|GFunHYGg zQ-K(d(?!499FUG3I?w_}gQ! zMQ%B?_{Cyvb2}V~F~)2CX(aqg2#o24QborbM5v+~sskmURXdyp%G8s#fpz_QRO)$@^TP1N8udHgseS9^b5by!# z3(K&h^7NrvN((+(&H(}YP-=$FWbxtkRQH@X3`~k}1U7qN?~8r7aUuYJ#2ts<5{vL5 zj^a_R2c+s*Jv0v)gPYd^ERcJEXM;rOxtYq;PyRSIONhgUeV;)l$C13+wuESYJ%WKR z%Ru{GI_I0NG|oN}#(d2GP688V5td^Hd`fF1o%YF8(zTTwY5zwGh6`|oVhYTY$e`-u zaa7Zm4Qn?7RJg9jpQpFNDgW7ccW?+IXISG|eIH`JY8sQUXR$vW&A~jDAk0%{Vfxle zDyKU{v@r{O9<)K#?M5t}8$(LlZ-eozZhZCg0Of76B%TLBaFYW4PEslV8{D7yf#l4CDi6(@y5;cgvADw5EEenwG~i1el_$@ghQ_B zOJ>iD&t&wp5c|cvbGT_B8y!Ba1WVNkkhG7aZ-rmL%#(MSi)LClBPR&iN=k5Nog4al zUuJ&axDQrR+0=LC6;SidM57zRxJAAKx1CI7)|8t->F;!Aa&|W8-UnoJor!_45MI}; zAw8o!FgQ;JSRyg#(&UJ%x2!?cY;W3%c#`UAX$|Lo6E?VS7JLLJQd)P$tR4 zEWQ>(Y}P%v{ZHgBZHoUwLK9w5*Z)$%Hc}FJPRc?@CofJ4deUg|^B74hXylCuyeM@A z3jUfgPtIwP_Bdy%WVM+jaOK&5uBFrCq08`Bx)9hGC~@!o4r5d-DJ8$_L=uiL2PZR0~X2pM~WLgVbe1 zKaIPZM7nfz&|=Xh{H?JcM%)*|tS5!=)cOKCT;##KKNCsNm^)rHUIMAyEcmfQ22-UL zG1Zp0;gs2X==w}x;^RCxkzk9aAFD}w>@3)LXeZrZI3JUE^YN?4O9bl)+_gg*g2_BG zziOQ3S6roOkvoZgxGIITYv^#>6+EgkNssa=66Ur8Bup(Zes?AuaG6c@0vw@l1`B+~ zBT$C&fWLSlQL*^Nnl!HGv<{VU+{7$kt-v(+3Tc7g zVe_4K*x2h0IwJ}=^<^7uy7#`8_fH|b?n(e--DOyn?+I<`S}-eYC0AQ?E9{VdL2o>E zz%TtfQ9ekN+T9-`zV~jRzsm-4=<*yWH2(@y(=}xuPdP396-mrro&i=vCYA9>hoIl% zSnY8E!$Zr+`_u{)-+m4q3k@KRlh4pZuc*y z_J==C`8i?e-Adwr$bpu~?ZkBAj<*ss>6Pkc5HA))ow5UUuhrWb|GP9}LGn z-XLkzv9GQSb%9h4&Y>d2+t@bV7$5{yw5MfZ?Oy5XCCJnRoPW9uK?k^Z-~pN1*+v4mL}tF_t}LkQwF5B;8z!;#$Wz4sR84#9#ri z!gcW5H9mxCPBFRM2x}J9piJBy=2zk^s`!}?Zp2<9zv}on*M(1T*6wFv|IH%ib>@C} zwAvs2pO@Bt{tyBFj=a#8RS5TvNn-Afk9f^*D;c;{L%dW=KuzX62p<%I>~Dt9<8DNM z%*?{hdEc4gm;f*mQk~{Kw!;IyXS9ASn#P5XlMjP4QABVCDkYyMh4(iASZ<+CZ_Z-D zUKY9~S`ev4Mnp--k7RiXz)5Fn;87V(#160Jl#8Um)Q)P1T6rEFO2gM0r& z{0>f8A0WLNX6RWuA2d0qi6&-}z=MMzpHWVt{4?pvjgz=%pBfcZFN9ZBw>gu0W^;F| z{i9D6gQ58LOgO@~6GN*c@ZhFSnpLQVn(ukJl_v~g=imj1*>eX|baUa!pH7Z)M;3fK zwvbu#P?Xe{P|~t~Gs=8A4=yo@c=K5+)zuY*Tw6ZeEM@`@a;2~^GZZd#j-${^9@2j; z9M;8N!Px1wpz5VMG%U@;jY-=;yAem8tBpwa+l9E9@GQD2BEUtsu z1{L&Qg9GG5A0m0Rg6!nu3HZ}73pS1P!-<|y)OOnhuhjj}MP{58Y!^;%HOX>WI=v+H zo)q!5;=l(lIq1ojVvFcMW0olPaQ@t5(I&~)^uDP$xBSF@TDx-w_v6t)vgLLTPK`LB z-@fG-+-FH;XTLUB*M1uKd*@(v*BFUz(qhZ4sVA15YiXdb3HJWoNO$x*;NSQd@?+K$ zrggt4cTDhnojS&Vlg&d)mZ)HeO(~XienGRx*5K2nPj&K);SgUVym@*EKK6*>3L9xs zzh52?Sn1&L$P%I}xg9wU;@qoy9Wh!Vo37y80hK+!sgF26{I$`bJ(F2bx9}_s#hn9< zA20BD$u`pZ-v$ue)j%f47lSYF5k{+sV9>f=6g66kD$dK<#)ZG=*49bod?gA9A< za4Okh=nP-SCt>5(BoN#Fj{2G9q5Susb<4L)Vz_bx&aCt$?8T?ZA_R+Y_QcPGPZrDirLoq;esm^rd$QyxJzi=Dsr`5}}i@Q^$`M zmwUnWkvOn<-$BFJfbv;qV3D*iE*f>hU-PtaOkM;xnC^zBz5K9WM-S#>H~AniAGiCg zg3GpPz$jk|B+C7DH0r$29SX(rZ@Z ztn$`{khby*eO+l!mC0?8P}pqnBTSDTn!3VNu$IzOmvwL;_$0E98sa5CJMdqxgbDeJ zsJG*4*c_8Ue|7=x6#a{ueRiWN6TW7kiHS9*iIZ1H1A(lQAGr_sB>tJkjg2so1 z!`TmNc*k9t7JWDdUrSwRRor@b%tUddulxZ{YctILcNH6M3P9gK3p}vEhzttEF`wr~ zqkCX9T-LH6ll95;tNANhrZk(xJ!>asKmKueHtBGk_nl@1PIE?eCw9<==5l;sUWxwm zb3pb@9^E?c0d*1|Co?IB-pLnaLrW~Z_5C) zRIMz*XG@wO^V0_O8ZsiBd-Ev!tvyb(R-l`1KYqp^%oU~q`xTS%p^+VB22{CvhfE;L zU;+1#PY!3z9AB0j6@j1{b&ktYUbaTqUUK(&6g*#}k2dDx%)PlyVC(EfSN0kaUD*Jr z=Of6wQ7Zo7iaG6-F z<4{4h%joiNnkQ~Z#k~f*V8}-qj(d4h)q*H`?$<}$^!Ex0E}z!Oonx^mQGw2vS;)j+ zmH}bL-+;Be9eIj`aO(_b;&ON=9FbpyK73;A`N1Q2#Y_UD#EUp9lDE?Cs%%s?dq+fh zrjfLl5p#6#D$$?lpFna{L~w-@XX`cw5udFNzQwa0w<-_Y3 z(|GI9HxksQ3uhwasnu~i>}$SQ$D=esdpb-pHK~}~o4Em(1vlgOnb$F2uN51;g2({> z0uu0|l}YuP3vy@9LvzkN?2vCK-7D^6l7A^&+>wi~?&)AKzby9dX~16|0!)s`ZgOR8 zJ$<_DE@t%q0jCTxn4hM~&R=MTclCTAIM@UXo6b_p>lC^T_G}-utr|Dh?fo?N_(Li!fn^uh|=~wanpOo&KKEL8n3QSuia7XaQe-r*VW< z#X^6oJ9s$u&}bb^$XO$TLLds>|6RtT$Fk7cD;mD@ABBBu7Q;W)3MOAvmYZOc&M7-R zL`>caqkyw6HV)i^wC}#0k9|WVKyxF^(+8=SZ$e7r88G0a;qrX~a3bRleBJhkzPWmi9?=xS$~Io?Ozc8^!6J+;%3;oO>OoIH z9%<|nJoca$_CJfqoV+J^PSzU9bv2B=-j93b`RV>vPgtIjkGgxx8JS!6iT&Lo&gx}^ zs7h=m-)320LI=Xhs$7_1ejXILq44E=3wlU-z{xHZcyzrHeI@tM{_1#QBisd{(fXie zy%&S{dcoCdHIwxxjEV#n(ktz4*!AcY$Fb@!8MnAgG&|HG1NM0hox*N#r zldb4@_Z3}O{S@9RP0`1}p)8)_n;euEIu_tJZK$FAS|4AEEL_K5j;v z2n1eZ5v`45bS2*#)Z(nbyhTw4%fwCLw@-B4tXZMN{VbQBecunfJHpU}cQx*rJ4K7X zio%|W2yCAaC2L-)63_aTDEZ?G5#tHP|E3af!QTf=zvmP5G7{l7xCp~#qbTU)8KyS` zm*f7*5cqfD4E_tdfmc;@U{G3`HL&#*ESePN{`X%w&ggJsS@(94)+aV_n140dlc`Gb z?e5X0OAnx5J062Ns)^|9K!ibI_*)l&m2Lz~(%vyUEc&7Cbs|+u?Ij6mCRpX1LbX$R znC}4%Sngek`;%m$B~6Bk+@FVv*I205z6$?@yl3dMH)QTR0WNk|KwsT8@KTfE7W_L6 zu9DHPHu-~ouE=HlI}!>e0)BK`xELG|n#IO%A!O{98xFs{M(4?H#=Rpih^MU?rpk^` zy@UDKv0jMk{98kkA1~)Vejh>9R~T@@pBmz`OcX%t|Haz>_}<4C}^I(1?TEEL5Dyt5xM>qG^7&XfB}b- zaX%CN2LjP*=rE}5QNo`>q4-*I10MRNjbe?v;9rO$ow!O@d(W(kP*@3aFNSzE+*U~+h}FLY}jd7hb0LTBv&mFe&mXQ*MgN$<@*qne4{xtR;r-T zhV!s#FcsE(o&{UhE};jzD%NfB$U|YD1UT_62G+hSL}mGX;P(Cz5o-@ay?q%Nz7EN& z+y_)gkz_ro_9AEz<2S!fdx1WaW`8x5KA#i~zKYEPCUqzs- zZUIsKUo7`*PMkgZG7ncI2iVaeYy&Y6ensG-s?8gRLUXne^g8OiGCI;KO*o~hyn&oyZ3 z@d7zs?o@H07d3x=rU|`r)7(@PID802xfdI7@y-Tn8X=9;umn=-lF@Aa9qKcqh!~{q z2Z541baTYmv=3938qY3zXcM$?Y*6xVOI@4& zcXYncLQU?c;c$jDSG`IUA|yK@?MxDE3fF?0LQiO?nHlI8W`W>BF(RLyhFePh(XsEn zaC}ZW=V7xow?v>0cUtz+h`^Jy&28IOXLGC}<2@f!z=f744@rDXS_C}LN50spj> z(~6Ge?D}WfxO}M^3~LK=eUKwM$*3Z*06T4zs_e$ z1z+YEpuoul&bp_L5PA6l@K##U3r2dJV-A_~etjke&A{8veYpKjpSxlEsK!z+} zmp^hK7S=adZ+RSHB5N7`{_%a?YpydhwU8fad^&u8Z%2h&oAAZn3|gjol1$V(;eRm} zc&U3PdRb<{J@*M}_N5A(Jmhix!OP^6|66)~=6c9dPDD`+0(;hcU_zfN?ZQ+^GeWCuf>aE=kVLZGmu?ym5jv+!xc|ooKy-xrG_9# zoaO*$nZ=^pojm+|;3a3lr7b8wZi(ue7clARFcoc0L0R26>}WbqV&^$<1UCF4(Q_V8 z>qJT{=$^q2$)J?GcpE%^X8~7&_QIa4>V*H31SE{Cg0$<4AyFt1Q)2t@@v|m$wK`2x zXU>ORd*jhM#vK->@}TSHDJnN}2i{D17HYIFK$puX3|X6l=C{4{&+pxEcu%mj9RC9vDS4fg zMNESCqi1zrPV=MZvopvY%cV|-myiJ)TP8f?IV?T(k1!&lROR&=+#Yj^l&(C8c}v$4 z%Y+G1Sg@CR+bZGUvzc7tI_;YYdPEAp^^pU8_b?%+6oyuOrIXK7sME_+n0Naveo;OP z`X%|G8=^*+eb&U`nf0`8nzQFkO~(_LQt90$IWlP#2D|NDS?}();JO)=WV=W>wJ_!7 plji^bGUxvv3;zE(h4B7w_X=Krzl|3B?8P7nY9 literal 0 HcmV?d00001 diff --git a/DeeployTest/Tests/Models/SleepConVit/network.onnx b/DeeployTest/Tests/Models/SleepConVit/network.onnx new file mode 100644 index 0000000000000000000000000000000000000000..c51390febeb65b39817a3c434bbe029fb769e4f5 GIT binary patch literal 118943 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 DY*E41oKbruvlt^B0SV~f-u>!SfJX4L3jO$ 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˴E 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 Xn1;thg+=&IHuZ_}3+2E54>kNBV>Yoj literal 0 HcmV?d00001 diff --git a/DeeployTest/Tests/Models/SleepConVit/outputs.npz b/DeeployTest/Tests/Models/SleepConVit/outputs.npz new file mode 100644 index 0000000000000000000000000000000000000000..8babb4ed7a100da2ff9f4255963231c5f851dfab GIT binary patch literal 286 zcmWIWW@Zs#fB;2?@A6;&P62X2n2SM#A-}YwptM9Uub`5VK>#cYQVEg;fysWMz5$Vp z3}p<}>M5zk$wlf`3hFj#Ch9s0>S_5!B}IvO@%cq5sUUH;#GK+(pm=dcVnHg9uVJX8 zV5+I3P^&;T;5z5ZXLsVobeodnvu*vtx9l Date: Sun, 15 Feb 2026 22:06:11 +0000 Subject: [PATCH 04/79] Add microbenchmark to codepass --- Deeploy/Targets/PULPOpen/Bindings.py | 4 +- .../PULPClusterTiling.py | 39 ++++- Deeploy/Targets/PULPOpen/Platform.py | 2 +- .../DoubleBufferingTilingCodeGeneration.py | 39 ++++- .../SingleBufferingTilingCodeGeneration.py | 40 ++++- .../TilingPrototypes.py | 99 +++++++++++ TargetLibraries/PULPOpen/inc/perf_utils.h | 158 ++++++++++++++++++ TargetLibraries/PULPOpen/src/Gemm.c | 23 --- .../PULPOpen/third_party/pulp-nn-mixed | 2 +- TargetLibraries/PULPOpen/third_party/pulp-nnx | 2 +- 10 files changed, 372 insertions(+), 36 deletions(-) create mode 100644 TargetLibraries/PULPOpen/inc/perf_utils.h diff --git a/Deeploy/Targets/PULPOpen/Bindings.py b/Deeploy/Targets/PULPOpen/Bindings.py index e1a9ed5932..61da33fc3c 100644 --- a/Deeploy/Targets/PULPOpen/Bindings.py +++ b/Deeploy/Targets/PULPOpen/Bindings.py @@ -102,7 +102,7 @@ PULPSynchCoresPass(), ForkClosure(writeback = False, generateStruct = True), TilingVariableReplacementUpdate("L1"), - PULPClusterTiling("L2", "L1", MchanDma()), + PULPClusterTiling("L2", "L1", MchanDma(), usePerfCounters=True), # Enable perf counters ArgumentStructGeneration(), MemoryManagementGeneration("L1"), TilingVariableReplacement("L2"), @@ -120,7 +120,7 @@ TilingVariableReplacement("L1"), TilingCallClosure(writeback = False, generateStruct = True), TilingVariableReplacementUpdate("L1"), - PULPClusterTiling("L2", "L1", MchanDma()), + PULPClusterTiling("L2", "L1", MchanDma(), usePerfCounters=True), # Enable perf counters ArgumentStructGeneration(), MemoryManagementGeneration("L1"), TilingVariableReplacement("L2"), diff --git a/Deeploy/Targets/PULPOpen/CodeTransformationPasses/PULPClusterTiling.py b/Deeploy/Targets/PULPOpen/CodeTransformationPasses/PULPClusterTiling.py index 3c0bba3107..59aec47a5d 100644 --- a/Deeploy/Targets/PULPOpen/CodeTransformationPasses/PULPClusterTiling.py +++ b/Deeploy/Targets/PULPOpen/CodeTransformationPasses/PULPClusterTiling.py @@ -7,9 +7,9 @@ from Deeploy.DeeployTypes import CodeGenVerbosity, CodeTransformationPass, ExecutionBlock, NetworkContext, _NoVerbosity from Deeploy.TilingExtension.AsyncDma import AsyncDma from Deeploy.TilingExtension.CodeTransformationPasses.DoubleBufferingTilingCodeGeneration import \ - DoubleBufferingTilingCodeGeneration, ProfilingDoubleBufferingTilingMixIn + DoubleBufferingTilingCodeGeneration, PerfCounterDoubleBufferingTilingMixIn, ProfilingDoubleBufferingTilingMixIn from Deeploy.TilingExtension.CodeTransformationPasses.SingleBufferingTilingCodeGeneration import \ - ProfilingSingleBufferingTilingMixIn, SingleBufferingTilingCodeGeneration + PerfCounterSingleBufferingTilingMixIn, ProfilingSingleBufferingTilingMixIn, SingleBufferingTilingCodeGeneration class PULPClusterTilingGenerationSB(SingleBufferingTilingCodeGeneration): @@ -28,13 +28,38 @@ class ProfilingPULPClusterTilingGenerationDB(DoubleBufferingTilingCodeGeneration pass +class PerfCounterPULPClusterTilingGenerationSB(SingleBufferingTilingCodeGeneration, PerfCounterSingleBufferingTilingMixIn): + """Single buffering with performance counter profiling""" + pass + + +class PerfCounterPULPClusterTilingGenerationDB(DoubleBufferingTilingCodeGeneration, PerfCounterDoubleBufferingTilingMixIn): + """Double buffering with performance counter profiling""" + pass + + +class CombinedProfilingPULPClusterTilingGenerationSB(SingleBufferingTilingCodeGeneration, ProfilingSingleBufferingTilingMixIn, PerfCounterSingleBufferingTilingMixIn): + """Single buffering with both cycle profiling and performance counter profiling""" + pass + + +class CombinedProfilingPULPClusterTilingGenerationDB(DoubleBufferingTilingCodeGeneration, ProfilingDoubleBufferingTilingMixIn, PerfCounterDoubleBufferingTilingMixIn): + """Double buffering with both cycle profiling and performance counter profiling""" + pass + + class PULPClusterTiling(CodeTransformationPass): - def __init__(self, externalMemory: str, localMemory: str, dma: AsyncDma): + def __init__(self, externalMemory: str, localMemory: str, dma: AsyncDma, usePerfCounters: bool = False): + self.usePerfCounters = usePerfCounters self.SB = PULPClusterTilingGenerationSB(externalMemory, localMemory, dma) self.profilingSB = ProfilingPULPClusterTilingGenerationSB(externalMemory, localMemory, dma) + self.perfCounterSB = PerfCounterPULPClusterTilingGenerationSB(externalMemory, localMemory, dma) + self.combinedProfilingSB = CombinedProfilingPULPClusterTilingGenerationSB(externalMemory, localMemory, dma) self.DB = PULPClusterTilingGenerationDB(externalMemory, localMemory, dma) self.profilingDB = ProfilingPULPClusterTilingGenerationDB(externalMemory, localMemory, dma) + self.perfCounterDB = PerfCounterPULPClusterTilingGenerationDB(externalMemory, localMemory, dma) + self.combinedProfilingDB = CombinedProfilingPULPClusterTilingGenerationDB(externalMemory, localMemory, dma) def apply(self, ctxt: NetworkContext, @@ -42,10 +67,16 @@ def apply(self, name: str, verbose: CodeGenVerbosity = _NoVerbosity) -> Tuple[NetworkContext, ExecutionBlock]: - if verbose.tilingProfiling: + if self.usePerfCounters and verbose.tilingProfiling: + # Use combined profiling: cycle measurements + performance counter stats + ctxt, executionBlock = self.combinedProfilingSB.apply(ctxt, executionBlock, name) + ctxt, executionBlock = self.combinedProfilingDB.apply(ctxt, executionBlock, name) + elif verbose.tilingProfiling: + # Use cycle profiling only (basic cycle measurements) ctxt, executionBlock = self.profilingSB.apply(ctxt, executionBlock, name) ctxt, executionBlock = self.profilingDB.apply(ctxt, executionBlock, name) else: + # No profiling ctxt, executionBlock = self.SB.apply(ctxt, executionBlock, name) ctxt, executionBlock = self.DB.apply(ctxt, executionBlock, name) diff --git a/Deeploy/Targets/PULPOpen/Platform.py b/Deeploy/Targets/PULPOpen/Platform.py index d45dc00f9c..85f8cdf5eb 100644 --- a/Deeploy/Targets/PULPOpen/Platform.py +++ b/Deeploy/Targets/PULPOpen/Platform.py @@ -245,7 +245,7 @@ class PULPStructBuffer(StructBuffer): # SCHEREMO: stdint is included before pulp_nn_kernels.h because it is supposed to be included in there, but isn't... _includeList = [ - "pmsis.h", "stdint.h", "pulp_nn_kernels.h", "DeeployPULPMath.h", "mchan_siracusa.h", "dory_mem.h", "bsp/ram.h" + "pmsis.h", "stdint.h", "pulp_nn_kernels.h", "DeeployPULPMath.h", "mchan_siracusa.h", "dory_mem.h", "bsp/ram.h", "perf_utils.h" ] diff --git a/Deeploy/TilingExtension/CodeTransformationPasses/DoubleBufferingTilingCodeGeneration.py b/Deeploy/TilingExtension/CodeTransformationPasses/DoubleBufferingTilingCodeGeneration.py index ad9c6ad012..ce9ec86f27 100644 --- a/Deeploy/TilingExtension/CodeTransformationPasses/DoubleBufferingTilingCodeGeneration.py +++ b/Deeploy/TilingExtension/CodeTransformationPasses/DoubleBufferingTilingCodeGeneration.py @@ -11,8 +11,8 @@ from Deeploy.TilingExtension.AsyncDma import AnydimAsyncDmaTransferAdapter, AsyncDma, Future from Deeploy.TilingExtension.CodeTransformationPasses.TilingCodeGeneration import TilingCodeGeneration from Deeploy.TilingExtension.CodeTransformationPasses.TilingHoistingMixIn import dictOfArrays -from Deeploy.TilingExtension.CodeTransformationPasses.TilingPrototypes import ProfilingPrototypeMixIn, \ - PrototypeTilingMixIn, TilingMetaInfo +from Deeploy.TilingExtension.CodeTransformationPasses.TilingPrototypes import PerfCounterProfilingMixIn, \ + ProfilingPrototypeMixIn, PrototypeTilingMixIn, TilingMetaInfo from Deeploy.TilingExtension.MemoryConstraints import NodeMemoryConstraint from Deeploy.TilingExtension.TilingCodegen import TilingSchedule, VariableReplacementScheme, stridesFromShape @@ -364,3 +364,38 @@ def generateLoopCode(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaIn executionBlock = super().generateLoopCode(executionBlock, metaInfo, _openLoopStatements, _ingressDMAStatements, _egressDMAStatements, closeLoopStatements) return executionBlock + +class PerfCounterDoubleBufferingTilingMixIn(PrototypeTilingMixIn, PerfCounterProfilingMixIn): + """ + Double buffering tiling with performance counter profiling. + Provides detailed instruction-level statistics for each tile. + """ + + @classmethod + def generateSetupAndTeardownCode(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaInfo, + setupStatements: List[CodeSnippet], + teardownStatements: List[CodeSnippet]) -> ExecutionBlock: + + executionBlock = super().generateSetupAndTeardownCode(executionBlock, metaInfo, setupStatements, + teardownStatements) + + # Inject performance counter initialization in setup (only once, not per-tile) + executionBlock = cls.injectPerfCounterInit(executionBlock, metaInfo) + + # Inject performance counter stop and print in teardown (only once, not per-tile) + executionBlock = cls.injectPerfCounterStop(executionBlock, metaInfo) + + return executionBlock + + @classmethod + def generateLoopCode(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaInfo, + openLoopStatements: List[CodeSnippet], ingressDMAStatements: List[CodeSnippet], + egressDMAStatements: List[CodeSnippet], + closeLoopStatements: List[CodeSnippet]) -> ExecutionBlock: + + # Don't wrap kernel - perf counters measure the whole tiling loop, not individual tiles + # executionBlock = cls.injectPerfCounterKernelWrap(executionBlock, metaInfo) + + executionBlock = super().generateLoopCode(executionBlock, metaInfo, openLoopStatements, ingressDMAStatements, + egressDMAStatements, closeLoopStatements) + return executionBlock diff --git a/Deeploy/TilingExtension/CodeTransformationPasses/SingleBufferingTilingCodeGeneration.py b/Deeploy/TilingExtension/CodeTransformationPasses/SingleBufferingTilingCodeGeneration.py index ea1e938b58..e4bb803611 100644 --- a/Deeploy/TilingExtension/CodeTransformationPasses/SingleBufferingTilingCodeGeneration.py +++ b/Deeploy/TilingExtension/CodeTransformationPasses/SingleBufferingTilingCodeGeneration.py @@ -10,8 +10,8 @@ from Deeploy.TilingExtension.AsyncDma import AsyncDma, DmaDirection, Future from Deeploy.TilingExtension.CodeTransformationPasses.TilingCodeGeneration import TilingCodeGeneration from Deeploy.TilingExtension.CodeTransformationPasses.TilingHoistingMixIn import dictOfArrays -from Deeploy.TilingExtension.CodeTransformationPasses.TilingPrototypes import ProfilingPrototypeMixIn, \ - PrototypeTilingMixIn, TilingMetaInfo +from Deeploy.TilingExtension.CodeTransformationPasses.TilingPrototypes import PerfCounterProfilingMixIn, \ + ProfilingPrototypeMixIn, PrototypeTilingMixIn, TilingMetaInfo from Deeploy.TilingExtension.MemoryConstraints import NodeMemoryConstraint, TensorMemoryConstraint from Deeploy.TilingExtension.TilingCodegen import HyperRectangle, TilingSchedule, VariableReplacementScheme @@ -191,3 +191,39 @@ def generateLoopCode(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaIn executionBlock = super().generateLoopCode(executionBlock, metaInfo, _openLoopStatements, _ingressDMAStatements, _egressDMAStatements, closeLoopStatements) return executionBlock + + +class PerfCounterSingleBufferingTilingMixIn(PrototypeTilingMixIn, PerfCounterProfilingMixIn): + """ + Single buffering tiling with performance counter profiling. + Provides detailed instruction-level statistics for each tile. + """ + + @classmethod + def generateSetupAndTeardownCode(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaInfo, + setupStatements: List[CodeSnippet], + teardownStatements: List[CodeSnippet]) -> ExecutionBlock: + + executionBlock = super().generateSetupAndTeardownCode(executionBlock, metaInfo, setupStatements, + teardownStatements) + + # Inject performance counter initialization in setup (only once, not per-tile) + executionBlock = cls.injectPerfCounterInit(executionBlock, metaInfo) + + # Inject performance counter stop and print in teardown (only once, not per-tile) + executionBlock = cls.injectPerfCounterStop(executionBlock, metaInfo) + + return executionBlock + + @classmethod + def generateLoopCode(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaInfo, + openLoopStatements: List[CodeSnippet], ingressDMAStatements: List[CodeSnippet], + egressDMAStatements: List[CodeSnippet], + closeLoopStatements: List[CodeSnippet]) -> ExecutionBlock: + + # Don't wrap kernel - perf counters measure the whole tiling loop, not individual tiles + # executionBlock = cls.injectPerfCounterKernelWrap(executionBlock, metaInfo) + + executionBlock = super().generateLoopCode(executionBlock, metaInfo, openLoopStatements, ingressDMAStatements, + egressDMAStatements, closeLoopStatements) + return executionBlock diff --git a/Deeploy/TilingExtension/CodeTransformationPasses/TilingPrototypes.py b/Deeploy/TilingExtension/CodeTransformationPasses/TilingPrototypes.py index 09a4ef56eb..70aabd9805 100644 --- a/Deeploy/TilingExtension/CodeTransformationPasses/TilingPrototypes.py +++ b/Deeploy/TilingExtension/CodeTransformationPasses/TilingPrototypes.py @@ -64,6 +64,105 @@ def generateAllTilingCode(cls, executionBlock: ExecutionBlock, metaInfo: TilingM return executionBlock +class PerfCounterProfilingMixIn(ABC): + """ + MixIn for injecting performance counter profiling code. + Provides detailed instruction-level statistics using CSR performance counters. + """ + + _perfCounterInit = NodeTemplate(""" + perf_stats_t ${nodeName}_perf_start, ${nodeName}_perf_end, ${nodeName}_perf_total; + if (pi_core_id() == 0) { + perf_bench_init(); + perf_bench_start(); + perf_bench_read(&${nodeName}_perf_start); + } + """) + + _perfCounterStop = NodeTemplate(""" + if (pi_core_id() == 0) { + perf_bench_stop(); + perf_bench_read(&${nodeName}_perf_end); + perf_bench_diff(&${nodeName}_perf_total, &${nodeName}_perf_end, &${nodeName}_perf_start); + perf_bench_print("${nodeName}", &${nodeName}_perf_total); + } + """) + + _perfCounterKernelStart = NodeTemplate(""" + if (pi_core_id() == 0) { + perf_bench_start(); + perf_bench_read(&${nodeName}_perf_kernel_start); + } + """) + + _perfCounterKernelEnd = NodeTemplate(""" + if (pi_core_id() == 0) { + perf_bench_stop(); + perf_bench_read(&${nodeName}_perf_kernel_end); + perf_bench_diff(&${nodeName}_perf_kernel_total, &${nodeName}_perf_kernel_end, &${nodeName}_perf_kernel_start); + perf_bench_print("${nodeName} Kernel", &${nodeName}_perf_kernel_total); + } + """) + + _perfCounterKernelDecl = NodeTemplate(""" + perf_stats_t ${nodeName}_perf_kernel_start, ${nodeName}_perf_kernel_end, ${nodeName}_perf_kernel_total; + """) + + @classmethod + def injectPerfCounterInit(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaInfo) -> ExecutionBlock: + """ + Inject performance counter initialization at the beginning of the node execution. + This should be called in the setup phase. + """ + nodeName = metaInfo.nodeName + + executionBlock.addLeft(cls._perfCounterInit, { + "nodeName": nodeName, + }) + + return executionBlock + + @classmethod + def injectPerfCounterStop(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaInfo) -> ExecutionBlock: + """ + Inject performance counter stop and print at the end of the node execution. + This should be called in the teardown phase. + """ + nodeName = metaInfo.nodeName + + executionBlock.addRight(cls._perfCounterStop, { + "nodeName": nodeName, + }) + + return executionBlock + + @classmethod + def injectPerfCounterKernelWrap(cls, executionBlock: ExecutionBlock, metaInfo: TilingMetaInfo) -> ExecutionBlock: + """ + Wrap the kernel execution with performance counter measurements. + This provides detailed statistics for just the kernel computation (excluding DMA). + """ + nodeName = metaInfo.nodeName + + if metaInfo.kernelLevelTiling: + # Add declaration at the beginning + executionBlock.addLeft(cls._perfCounterKernelDecl, { + "nodeName": nodeName, + }) + + # Add start measurement before kernel + executionBlock.addLeft(cls._perfCounterKernelStart, { + "nodeName": nodeName, + }) + + # Add stop and print after kernel + executionBlock.addRight(cls._perfCounterKernelEnd, { + "nodeName": nodeName, + }) + + return executionBlock + + class ProfilingPrototypeMixIn(ABC): _measureCycles = NodeTemplate(""" ${measurements}[${tileIdxVar}] = getCycles(); diff --git a/TargetLibraries/PULPOpen/inc/perf_utils.h b/TargetLibraries/PULPOpen/inc/perf_utils.h new file mode 100644 index 0000000000..2d9fbc39c6 --- /dev/null +++ b/TargetLibraries/PULPOpen/inc/perf_utils.h @@ -0,0 +1,158 @@ +/* + * Performance Counter Utilities for PULP Benchmarking + */ + +#ifndef __PERF_UTILS_H__ +#define __PERF_UTILS_H__ + +#include "pmsis.h" + +// Performance event IDs (compatible with PMSIS) +#define PI_PERF_CYCLES CSR_PCER_CYCLES +#define PI_PERF_INSTR CSR_PCER_INSTR +#define PI_PERF_LD_STALL CSR_PCER_LD_STALL +#define PI_PERF_JMP_STALL CSR_PCER_JMP_STALL +#define PI_PERF_IMISS CSR_PCER_IMISS +#define PI_PERF_LD CSR_PCER_LD +#define PI_PERF_ST CSR_PCER_ST +#define PI_PERF_JUMP CSR_PCER_JUMP +#define PI_PERF_BRANCH CSR_PCER_BRANCH +#define PI_PERF_TAKEN_BRANCH CSR_PCER_TAKEN_BRANCH +#define PI_PERF_RVC CSR_PCER_RVC +#define PI_PERF_LD_EXT CSR_PCER_LD_EXT +#define PI_PERF_ST_EXT CSR_PCER_ST_EXT +#define PI_PERF_LD_EXT_CYC CSR_PCER_LD_EXT_CYC +#define PI_PERF_ST_EXT_CYC CSR_PCER_ST_EXT_CYC +#define PI_PERF_TCDM_CONT CSR_PCER_TCDM_CONT + +// Benchmark statistics structure +typedef struct { + unsigned int cycles; + unsigned int instr; + unsigned int ld; + unsigned int st; + unsigned int ld_stall; + unsigned int jmp_stall; + unsigned int imiss; + unsigned int branch; + unsigned int taken_branch; + unsigned int rvc; + unsigned int ld_ext; + unsigned int st_ext; + unsigned int ld_ext_cyc; + unsigned int st_ext_cyc; + unsigned int tcdm_cont; +} perf_stats_t; + +// Initialize performance counters for comprehensive benchmarking +static inline void perf_bench_init() { + // Enable all performance counters + pi_perf_conf( + (1 << PI_PERF_CYCLES) | + (1 << PI_PERF_INSTR) | + (1 << PI_PERF_LD_STALL) | + (1 << PI_PERF_JMP_STALL) | + (1 << PI_PERF_IMISS) | + (1 << PI_PERF_LD) | + (1 << PI_PERF_ST) | + (1 << PI_PERF_JUMP) | + (1 << PI_PERF_BRANCH) | + (1 << PI_PERF_TAKEN_BRANCH) | + (1 << PI_PERF_RVC) | + (1 << PI_PERF_LD_EXT) | + (1 << PI_PERF_ST_EXT) | + (1 << PI_PERF_LD_EXT_CYC) | + (1 << PI_PERF_ST_EXT_CYC) | + (1 << PI_PERF_TCDM_CONT) + ); +} + +// Start performance monitoring +static inline void perf_bench_start() { + pi_perf_reset(); + pi_perf_start(); +} + +// Stop performance monitoring +static inline void perf_bench_stop() { + pi_perf_stop(); +} + +// Read all performance counters into structure +static inline void perf_bench_read(perf_stats_t *stats) { + stats->cycles = pi_perf_read(PI_PERF_CYCLES); + stats->instr = pi_perf_read(PI_PERF_INSTR); + stats->ld = pi_perf_read(PI_PERF_LD); + stats->st = pi_perf_read(PI_PERF_ST); + stats->ld_stall = pi_perf_read(PI_PERF_LD_STALL); + stats->jmp_stall = pi_perf_read(PI_PERF_JMP_STALL); + stats->imiss = pi_perf_read(PI_PERF_IMISS); + stats->branch = pi_perf_read(PI_PERF_BRANCH); + stats->taken_branch = pi_perf_read(PI_PERF_TAKEN_BRANCH); + stats->rvc = pi_perf_read(PI_PERF_RVC); + stats->ld_ext = pi_perf_read(PI_PERF_LD_EXT); + stats->st_ext = pi_perf_read(PI_PERF_ST_EXT); + stats->ld_ext_cyc = pi_perf_read(PI_PERF_LD_EXT_CYC); + stats->st_ext_cyc = pi_perf_read(PI_PERF_ST_EXT_CYC); + stats->tcdm_cont = pi_perf_read(PI_PERF_TCDM_CONT); +} + +// Print performance statistics (core 0 only to avoid clutter) +static inline void perf_bench_print(const char *label, perf_stats_t *stats) { + if (pi_core_id() == 0) { + printf("\n=== Performance Statistics: %s ===\n", label); + printf("Cycles: %10u\n", stats->cycles); + printf("Instructions: %10u\n", stats->instr); + printf("IPC: %10.3f\n", + stats->cycles > 0 ? (float)stats->instr / stats->cycles : 0.0f); + printf("\n--- Instruction Mix ---\n"); + printf("Loads: %10u (%.2f%%)\n", stats->ld, + stats->instr > 0 ? 100.0f * stats->ld / stats->instr : 0.0f); + printf("Stores: %10u (%.2f%%)\n", stats->st, + stats->instr > 0 ? 100.0f * stats->st / stats->instr : 0.0f); + printf("Branches: %10u (%.2f%%)\n", stats->branch, + stats->instr > 0 ? 100.0f * stats->branch / stats->instr : 0.0f); + printf("Taken Branches: %10u (%.2f%%)\n", stats->taken_branch, + stats->branch > 0 ? 100.0f * stats->taken_branch / stats->branch : 0.0f); + printf("Compressed (RVC): %10u (%.2f%%)\n", stats->rvc, + stats->instr > 0 ? 100.0f * stats->rvc / stats->instr : 0.0f); + printf("\n--- Stalls & Hazards ---\n"); + printf("Load Stalls: %10u\n", stats->ld_stall); + printf("Jump Stalls: %10u\n", stats->jmp_stall); + printf("I-cache Misses: %10u\n", stats->imiss); + printf("TCDM Contentions: %10u\n", stats->tcdm_cont); + printf("\n--- Memory Hierarchy ---\n"); + printf("External Loads: %10u (%.2f%%)\n", stats->ld_ext, + stats->ld > 0 ? 100.0f * stats->ld_ext / stats->ld : 0.0f); + printf("External Stores: %10u (%.2f%%)\n", stats->st_ext, + stats->st > 0 ? 100.0f * stats->st_ext / stats->st : 0.0f); + printf("Ext Load Cycles: %10u (avg: %.2f)\n", stats->ld_ext_cyc, + stats->ld_ext > 0 ? (float)stats->ld_ext_cyc / stats->ld_ext : 0.0f); + printf("Ext Store Cycles: %10u (avg: %.2f)\n", stats->st_ext_cyc, + stats->st_ext > 0 ? (float)stats->st_ext_cyc / stats->st_ext : 0.0f); + printf("========================================\n\n"); + } +} + +// Compute difference between two stats (for analyzing specific code sections) +static inline void perf_bench_diff(perf_stats_t *result, + perf_stats_t *end, + perf_stats_t *start) { + result->cycles = end->cycles - start->cycles; + result->instr = end->instr - start->instr; + result->ld = end->ld - start->ld; + result->st = end->st - start->st; + result->ld_stall = end->ld_stall - start->ld_stall; + result->jmp_stall = end->jmp_stall - start->jmp_stall; + result->imiss = end->imiss - start->imiss; + result->branch = end->branch - start->branch; + result->taken_branch = end->taken_branch - start->taken_branch; + result->rvc = end->rvc - start->rvc; + result->ld_ext = end->ld_ext - start->ld_ext; + result->st_ext = end->st_ext - start->st_ext; + result->ld_ext_cyc = end->ld_ext_cyc - start->ld_ext_cyc; + result->st_ext_cyc = end->st_ext_cyc - start->st_ext_cyc; + result->tcdm_cont = end->tcdm_cont - start->tcdm_cont; +} + +#endif // __PERF_UTILS_H__ diff --git a/TargetLibraries/PULPOpen/src/Gemm.c b/TargetLibraries/PULPOpen/src/Gemm.c index 02fd991674..a46f8ac6ae 100644 --- a/TargetLibraries/PULPOpen/src/Gemm.c +++ b/TargetLibraries/PULPOpen/src/Gemm.c @@ -6,7 +6,6 @@ #include "DeeployPULPMath.h" #include "pmsis.h" -// #include "perf_utils.h" void PULP_Gemm_fp32_fp32_fp32_fp32(const float32_t *__restrict__ pSrcA, const float32_t *__restrict__ pSrcB, @@ -18,16 +17,6 @@ void PULP_Gemm_fp32_fp32_fp32_fp32(const float32_t *__restrict__ pSrcA, int8_t core_id = pi_core_id(); int8_t log2Core = LOG2(NUM_CORES); - //RW: Performance monitoring is currently disabled - // perf_stats_t perf_start, perf_end, perf_total; - - // // Initialize and start performance counters (only core 0) - // if (core_id == 0) { - // perf_bench_init(); - // perf_bench_start(); - // perf_bench_read(&perf_start); - // } - uint32_t M_chunk = (M >> log2Core) + ((M & (NUM_CORES - 1)) != 0); uint32_t M_start = MIN(core_id * M_chunk, M); uint32_t M_end = MIN(M_start + M_chunk, M); @@ -362,16 +351,4 @@ void PULP_Gemm_fp32_fp32_fp32_fp32(const float32_t *__restrict__ pSrcA, } } } - - // RW: Stop performance counters and print results (only core 0) - // if (core_id == 0) { - // perf_bench_stop(); - // perf_bench_read(&perf_end); - // perf_bench_diff(&perf_total, &perf_end, &perf_start); - - // char label[100]; - // snprintf(label, sizeof(label), "GEMM M=%u N=%u O=%u transA=%u transB=%u", - // M, N, O, transA, transB); - // perf_bench_print(label, &perf_total); - // } } \ No newline at end of file diff --git a/TargetLibraries/PULPOpen/third_party/pulp-nn-mixed b/TargetLibraries/PULPOpen/third_party/pulp-nn-mixed index a9b4aaf597..faed38c72b 160000 --- a/TargetLibraries/PULPOpen/third_party/pulp-nn-mixed +++ b/TargetLibraries/PULPOpen/third_party/pulp-nn-mixed @@ -1 +1 @@ -Subproject commit a9b4aaf597c030ce24bf65a00b5f3ec84a1528c4 +Subproject commit faed38c72b029b69dcab98571d228a66c3263891 diff --git a/TargetLibraries/PULPOpen/third_party/pulp-nnx b/TargetLibraries/PULPOpen/third_party/pulp-nnx index 234971fca4..c4f6ba351e 160000 --- a/TargetLibraries/PULPOpen/third_party/pulp-nnx +++ b/TargetLibraries/PULPOpen/third_party/pulp-nnx @@ -1 +1 @@ -Subproject commit 234971fca4a0eba5e8b703e9ccb62b7764dac7fa +Subproject commit c4f6ba351e30b31125baba35896db394804d819d From bedec915effe3d7ba8433bf9ee1bd9658560a574 Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Thu, 12 Feb 2026 19:28:08 +0100 Subject: [PATCH 05/79] Add GAP9 Container Support - Cleanup Docker flow to use a temporary build folder - Install zsh and oh-my-zsh plugin - Adapt GAP9 Docker Flow to support ARM64 - Add GAP9 Docker GitHub Build Flow - Add GAP9 Run script to use real hardware - Update README - Fix missing pre-commit dependency WIP --- .../workflows/docker-build-deeploy-gap9.yml | 155 +++++ .gitignore | 5 + .pre-commit-config.yaml | 1 + Container/.zshrc | 116 ++++ Container/Dockerfile.deeploy | 35 +- Container/Dockerfile.deeploy-gap9 | 121 ++++ Container/Dockerfile.toolchain | 11 +- Container/Makefile | 52 +- Container/gap9-amd64.list | 8 + Container/gap9-amd64.patch | 38 ++ Container/gap9-arm64.patch | 366 ++++++++++++ Container/gap9-sources.list | 46 ++ Makefile | 37 +- README_GAP9.md | 99 ++++ scripts/gap9-build_sdk.sh | 72 +++ scripts/gap9-run.sh | 543 ++++++++++++++++++ 16 files changed, 1674 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/docker-build-deeploy-gap9.yml create mode 100644 Container/.zshrc create mode 100644 Container/Dockerfile.deeploy-gap9 create mode 100644 Container/gap9-amd64.list create mode 100644 Container/gap9-amd64.patch create mode 100644 Container/gap9-arm64.patch create mode 100644 Container/gap9-sources.list create mode 100644 README_GAP9.md create mode 100755 scripts/gap9-build_sdk.sh create mode 100755 scripts/gap9-run.sh diff --git a/.github/workflows/docker-build-deeploy-gap9.yml b/.github/workflows/docker-build-deeploy-gap9.yml new file mode 100644 index 0000000000..e6a9ab7016 --- /dev/null +++ b/.github/workflows/docker-build-deeploy-gap9.yml @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +--- +name: Docker • Build Deeploy GAP9 Container + +"on": + workflow_dispatch: + inputs: + docker_image_deeploy: + description: "Deeploy Image to use" + required: false + default: "ghcr.io/pulp-platform/deeploy:latest" + +jobs: + prepare: + name: Fetch branch name or tag + runs-on: ubuntu-latest + outputs: + docker_tag: ${{ steps.generate_tag.outputs.docker_tag }} + steps: + - uses: actions/checkout@v4 + + - name: Set up environment variables + run: | + echo "BRANCH_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV + echo "TAG_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV + echo "IS_TAG=${GITHUB_REF_TYPE}" >> $GITHUB_ENV + + - name: Set Docker tag + id: generate_tag + run: | + if [[ "${{ env.IS_TAG }}" == "tag" ]]; then + echo "docker_tag=${{ env.TAG_NAME }}" >> $GITHUB_OUTPUT + else + echo "docker_tag=${{ env.BRANCH_NAME }}" >> $GITHUB_OUTPUT + fi + + build-deeploy-gap9: + name: Build Deeploy GAP9 Image + needs: [prepare] + runs-on: ${{ matrix.runner }} + outputs: + digest-amd64: ${{ steps.digest.outputs.digest-amd64 }} + digest-arm64: ${{ steps.digest.outputs.digest-arm64 }} + strategy: + fail-fast: false + matrix: + platform: [amd64, arm64] + include: + - platform: amd64 + runner: ubuntu-latest + - platform: arm64 + runner: ubuntu-22.04-arm + steps: + - uses: actions/checkout@v4 + + - name: Free up disk space + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: true + android: true + dotnet: true + haskell: true + large-packages: true + + - uses: docker/setup-buildx-action@v3 + + - name: GHCR Log-in + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Cache for Docker + id: cache + uses: actions/cache@v4 + with: + path: var-ccache + key: ${{ runner.os }}-${{ matrix.platform }}-build-cache-deeploy-gap9 + + - name: Inject build-cache + uses: reproducible-containers/buildkit-cache-dance@v3.1.0 + with: + cache-map: | + { + "var-ccache": "/ccache" + } + skip-extraction: ${{ steps.cache.outputs.cache-hit }} + + - name: Lower Case Repository Name + run: | + echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + + - name: Build and push final deploy image + id: build + uses: docker/build-push-action@v6 + with: + platforms: linux/${{ matrix.platform }} + context: . + cache-from: type=gha + cache-to: type=gha,mode=min + file: Container/Dockerfile.deeploy-gap9 + push: true + build-args: | + DEEPLOY_IMAGE=${{ github.event.inputs.docker_image_deeploy }} + outputs: type=image,name=ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9,annotation-index=true,name-canonical=true,push=true + + - name: Extract image digest + id: digest + run: echo "digest-${{ matrix.platform }}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT + + merge-deeploy-gap9-images: + name: Merge Deeploy GAP9 Images + runs-on: ubuntu-latest + needs: [prepare, build-deeploy-gap9] + steps: + - name: GHCR Log-in + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lower Case Repository Name + run: | + echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + + - name: Merge Deeploy GAP9 Images + uses: Noelware/docker-manifest-action@v1 + with: + inputs: | + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9@${{ needs.build-deeploy-gap9.outputs.digest-amd64 }}, + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9@${{ needs.build-deeploy-gap9.outputs.digest-arm64 }} + tags: | + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9:latest, + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9:${{ needs.prepare.outputs.docker_tag }} + push: true + + - name: Set package visibility to internal + uses: actions/github-script@v7 + with: + script: | + await github.rest.packages.updatePackageVersionVisibility({ + package_type: 'container', + package_name: 'deeploy-gap9', + visibility: 'internal', + org: context.repo.owner + }); diff --git a/.gitignore b/.gitignore index dc93328e4a..d9e4faace3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ install/ compile_commands.json toolchain/**/*/ + # Node package.json package-lock.json @@ -52,3 +53,7 @@ DeeployTest/Tests/**/generateTest.py DeeployTest/out.txt CHANGELOG_GEN.md + +# Container Artifacts +.pyusbip/ +.cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1d58d3265..e6f07fbc8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,7 @@ exclude: | | .*TEST_.* | .*TestFiles.* | .*runtime.* + | .*\.patch | .*prebuilt/.* ) diff --git a/Container/.zshrc b/Container/.zshrc new file mode 100644 index 0000000000..c690527643 --- /dev/null +++ b/Container/.zshrc @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +# If you come from bash you might have to change your $PATH. +# export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH + +# Path to your Oh My Zsh installation. +export ZSH="$HOME/.oh-my-zsh" + +# Set name of the theme to load --- if set to "random", it will +# load a random theme each time Oh My Zsh is loaded, in which case, +# to know which specific one was loaded, run: echo $RANDOM_THEME +# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes +ZSH_THEME="cypher" + +# Set list of themes to pick from when loading at random +# Setting this variable when ZSH_THEME=random will cause zsh to load +# a theme from this variable instead of looking in $ZSH/themes/ +# If set to an empty array, this variable will have no effect. +# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) + +# Uncomment the following line to use case-sensitive completion. +# CASE_SENSITIVE="true" + +# Uncomment the following line to use hyphen-insensitive completion. +# Case-sensitive completion must be off. _ and - will be interchangeable. +# HYPHEN_INSENSITIVE="true" + +# Uncomment one of the following lines to change the auto-update behavior +# zstyle ':omz:update' mode disabled # disable automatic updates +# zstyle ':omz:update' mode auto # update automatically without asking +# zstyle ':omz:update' mode reminder # just remind me to update when it's time + +# Uncomment the following line to change how often to auto-update (in days). +# zstyle ':omz:update' frequency 13 + +# Uncomment the following line if pasting URLs and other text is messed up. +# DISABLE_MAGIC_FUNCTIONS="true" + +# Uncomment the following line to disable colors in ls. +# DISABLE_LS_COLORS="true" + +# Uncomment the following line to disable auto-setting terminal title. +# DISABLE_AUTO_TITLE="true" + +# Uncomment the following line to enable command auto-correction. +# ENABLE_CORRECTION="true" + +# Uncomment the following line to display red dots whilst waiting for completion. +# You can also set it to another string to have that shown instead of the default red dots. +# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f" +# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765) +# COMPLETION_WAITING_DOTS="true" + +# Uncomment the following line if you want to disable marking untracked files +# under VCS as dirty. This makes repository status check for large repositories +# much, much faster. +# DISABLE_UNTRACKED_FILES_DIRTY="true" + +# Uncomment the following line if you want to change the command execution time +# stamp shown in the history command output. +# You can set one of the optional three formats: +# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" +# or set a custom format using the strftime function format specifications, +# see 'man strftime' for details. +# HIST_STAMPS="mm/dd/yyyy" + +# Would you like to use another custom folder than $ZSH/custom? +# ZSH_CUSTOM=/path/to/new-custom-folder + +# Which plugins would you like to load? +# Standard plugins can be found in $ZSH/plugins/ +# Custom plugins may be added to $ZSH_CUSTOM/plugins/ +# Example format: plugins=(rails git textmate ruby lighthouse) +# Add wisely, as too many plugins slow down shell startup. +plugins=(git) + +source $ZSH/oh-my-zsh.sh + +# User configuration + +# export MANPATH="/usr/local/man:$MANPATH" + +# You may need to manually set your language environment +# export LANG=en_US.UTF-8 + +# Preferred editor for local and remote sessions +# if [[ -n $SSH_CONNECTION ]]; then +# export EDITOR='vim' +# else +# export EDITOR='nvim' +# fi + +# Compilation flags +# export ARCHFLAGS="-arch $(uname -m)" + +# Set personal aliases, overriding those provided by Oh My Zsh libs, +# plugins, and themes. Aliases can be placed here, though Oh My Zsh +# users are encouraged to define aliases within a top-level file in +# the $ZSH_CUSTOM folder, with .zsh extension. Examples: +# - $ZSH_CUSTOM/aliases.zsh +# - $ZSH_CUSTOM/macos.zsh +# For a full list of active aliases, run `alias`. +# +# Example aliases +alias zshconfig="nano ~/.zshrc" +# alias ohmyzsh="mate ~/.oh-my-zsh" + +unsetopt HIST_SAVE_BY_COPY +setopt APPEND_HISTORY +setopt SHARE_HISTORY +setopt HIST_IGNORE_ALL_DUPS + +# Make sure the gap command of the GAP9 SDK works +unalias gap diff --git a/Container/Dockerfile.deeploy b/Container/Dockerfile.deeploy index ee5adc5e4f..2b717e49ec 100644 --- a/Container/Dockerfile.deeploy +++ b/Container/Dockerfile.deeploy @@ -14,15 +14,17 @@ ARG TARGETPLATFORM ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 ENV PATH="/app/install/bender:${PATH}" +ENV DEEPLOY_INSTALL_DIR=/app/install ENV LLVM_INSTALL_DIR=/app/install/llvm -WORKDIR /app +WORKDIR /app/build # Make sure updates in the repo are reflected in the image COPY toolchain/*.patch toolchain/ COPY Makefile ./ -COPY requirements-dev.txt ./ # Compile emulators # WIESEP: We need to already clean up some space, otherwise the GitHub runners run out of disk space @@ -30,8 +32,8 @@ RUN --mount=type=cache,target=/ccache \ ccache -z && \ make pulp-sdk chimera-sdk qemu mempool banshee xtensor && \ make gvsoc && \ - cp -r /app/toolchain/gvsoc/core/requirements.txt /app/core-requirements.txt && \ - cp -r /app/toolchain/gvsoc/gapy/requirements.txt /app/gapy-requirements.txt && \ + cp -r toolchain/gvsoc/core/requirements.txt /app/core-requirements.txt && \ + cp -r toolchain/gvsoc/gapy/requirements.txt /app/gapy-requirements.txt && \ rm -rf toolchain/pulp-sdk toolchain/qemu toolchain/mempool toolchain/banshee toolchain/xtensor toolchain/xtl toolchain/xsimd toolchain/gvsoc && \ ccache -s @@ -54,13 +56,15 @@ fi # Compile Snitch Runtime ENV CC="gcc" ENV CXX="g++" +ENV PATH=/app/install/bender:${PATH} RUN --mount=type=cache,target=/ccache \ ccache -z && \ make snitch_runtime && \ ccache -s # Remove toolchain to make the container lighter -RUN rm -rf toolchain +WORKDIR /app +RUN rm -rf /app/build ########## Stage 2: Lightweight image with precompiled toolchain and emulators ########## FROM ubuntu:22.04 AS deeploy @@ -68,9 +72,12 @@ FROM ubuntu:22.04 AS deeploy ARG TARGETPLATFORM ARG DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 # Export symbols necessary for Deeploy's build flow ENV CMAKE=/usr/bin/cmake +ENV DEEPLOY_INSTALL_DIR=/app/install ENV PULP_SDK_HOME=/app/install/pulp-sdk ENV SNITCH_HOME=/app/install/snitch_cluster ENV CHIMERA_SDK_HOME=/app/install/chimera-sdk @@ -83,7 +90,7 @@ ENV MEMPOOL_HOME=/app/install/mempool ENV BENDER_INSTALL_DIR=/app/install/bender ENV PATH=/app/install/qemu/bin:/app/install/banshee:/app/install/bender:$PATH -WORKDIR /app +WORKDIR /app/build COPY pyproject.toml ./ @@ -99,7 +106,8 @@ RUN apt-get update && \ python-is-python3 \ python3.10-venv \ python3.10-distutils \ - gcc && \ + gcc \ + zsh && \ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ python get-pip.py && \ rm get-pip.py && \ @@ -118,10 +126,19 @@ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ ./cmake-3.24.0-linux-aarch64.sh --prefix=/usr --skip-license; \ fi +COPY Container/.zshrc ./ +# # Install Oh My ZSH +RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended && \ + cp .zshrc /root/.zshrc + COPY --from=toolchain /app/core-requirements.txt ./core-requirements.txt COPY --from=toolchain /app/gapy-requirements.txt ./gapy-requirements.txt -COPY --from=toolchain /app/requirements-dev.txt ./ +COPY requirements-dev.txt ./ RUN pip install -r requirements-dev.txt -r core-requirements.txt -r gapy-requirements.txt # Copy pre-built toolchains and emulators -COPY --from=toolchain /app/install ./install +COPY --from=toolchain /app/install /app/install + +# Remove unused files and clean up to reduce image size +WORKDIR /app +RUN rm -rf /app/build diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 new file mode 100644 index 0000000000..5232636d0c --- /dev/null +++ b/Container/Dockerfile.deeploy-gap9 @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +ARG DEEPOY_IMAGE=ghcr.io/pulp-platform/deeploy:latest +FROM ${DEEPOY_IMAGE} AS deeploy + +ARG TARGETPLATFORM +ARG DEBIAN_FRONTEND=noninteractive + +ENV GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 +ENV GAP_SDK_HOME=/app/install/gap9-sdk +ENV GAP9_SDK_INSTALL_DIR=/app/install/gap9-sdk +ENV GAP_RISCV_GCC_INSTALL_DIR=/app/install/gcc/gap9 + +WORKDIR /app/build + +# Install SSH keys to access private repositories +RUN mkdir -p -m 0700 ~/.ssh && \ + ssh-keyscan iis-git.ee.ethz.ch >> ~/.ssh/known_hosts && \ + ssh-keyscan github.com >> ~/.ssh/known_hosts + +COPY toolchain/*.patch toolchain/ +COPY Makefile ./ + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + git-lfs \ + ccache \ + ninja-build \ + pkg-config \ + ibglib2.0-dev \ + libpixman-1-dev \ + python3 \ + python3-pip \ + python3-dev \ + python-is-python3 \ + python3.10-venv \ + python3.10-distutils \ + curl \ + protobuf-compiler \ + libftdi-dev \ + libftdi1 \ + doxygen \ + libsdl2-dev \ + scons \ + gtkwave \ + libsndfile1-dev \ + rsync \ + autoconf \ + automake \ + texinfo \ + libtool \ + libsdl2-ttf-dev \ + gcc \ + wget \ + clang-format \ + g++ \ + sudo \ + device-tree-compiler \ + zsh \ + nano \ + sudo \ + gdb-multiarch \ + ssh \ + gcc-x86-64-linux-gnu \ + telnet \ + usbutils \ + libftdi-dev \ + libusb-1.0-0-dev && \ + rm -rf /var/lib/apt/lists/* + + +COPY Container/gap9-amd64.list Container/gap9-sources.list ./ +# Install AMD64 libraries on ARM64 for GCC GAP9 compiler support +RUN <traces.new_trace(itf_name, &this->trace, vp::DEBUG); + top->new_master_port("sfu_irq", &this->irq); ++ top->new_master_port("sfu_pdm_out_0", &this->irq); ++ top->new_master_port("sfu_pdm_out_1", &this->irq); ++ top->new_master_port("sfu_pdm_out_2", &this->irq); ++ top->new_master_port("sfu_pdm_out_3", &this->irq); ++ top->new_master_port("sfu_pdm_out_4", &this->irq); ++ top->new_master_port("sfu_pdm_out_5", &this->irq); ++ ++ top->new_master_port("sfu_pdm_in_0", &this->irq); ++ top->new_master_port("sfu_pdm_in_1", &this->irq); ++ top->new_master_port("sfu_pdm_in_2", &this->irq); ++ top->new_master_port("sfu_pdm_in_3", &this->irq); ++ top->new_master_port("sfu_pdm_in_4", &this->irq); ++ top->new_master_port("sfu_pdm_in_5", &this->irq); ++ top->new_master_port("sfu_pdm_in_6", &this->irq); ++ top->new_master_port("sfu_pdm_in_7", &this->irq); ++ top->new_master_port("sfu_pdm_in_8", &this->irq); ++ top->new_master_port("sfu_pdm_in_9", &this->irq); ++ top->new_master_port("sfu_pdm_in_10", &this->irq); ++ top->new_master_port("sfu_pdm_in_11", &this->irq); ++ ++ top->new_master_port("sfu_ws_in_0", &this->irq); ++ top->new_master_port("sfu_ws_in_1", &this->irq); ++ top->new_master_port("sfu_ws_in_2", &this->irq); ++ ++ ++ // top->new_slave_port("sfu_stream_in_ready_0", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_1", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_2", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_3", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_4", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_5", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_6", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_7", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_8", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_9", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_10", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_11", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_12", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_13", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_14", &this->in); ++ // top->new_slave_port("sfu_stream_in_ready_15", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_0", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_1", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_2", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_3", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_4", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_5", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_6", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_7", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_8", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_9", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_10", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_11", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_12", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_13", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_14", &this->in); ++ // top->new_slave_port("sfu_stream_out_data_15", &this->in); ++ ++ top->new_master_port("sfu_stream_in_data_0", &this->irq); ++ top->new_master_port("sfu_stream_in_data_1", &this->irq); ++ top->new_master_port("sfu_stream_in_data_2", &this->irq); ++ top->new_master_port("sfu_stream_in_data_3", &this->irq); ++ top->new_master_port("sfu_stream_in_data_4", &this->irq); ++ top->new_master_port("sfu_stream_in_data_5", &this->irq); ++ top->new_master_port("sfu_stream_in_data_6", &this->irq); ++ top->new_master_port("sfu_stream_in_data_7", &this->irq); ++ top->new_master_port("sfu_stream_in_data_8", &this->irq); ++ top->new_master_port("sfu_stream_in_data_9", &this->irq); ++ top->new_master_port("sfu_stream_in_data_10", &this->irq); ++ top->new_master_port("sfu_stream_in_data_11", &this->irq); ++ top->new_master_port("sfu_stream_in_data_12", &this->irq); ++ top->new_master_port("sfu_stream_in_data_13", &this->irq); ++ top->new_master_port("sfu_stream_in_data_14", &this->irq); ++ top->new_master_port("sfu_stream_in_data_15", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_0", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_1", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_2", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_3", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_4", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_5", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_6", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_7", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_8", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_9", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_10", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_11", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_12", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_13", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_14", &this->irq); ++ top->new_master_port("sfu_stream_out_ready_15", &this->irq); + } + + +diff --git a/install_python_deps.sh b/install_python_deps.sh +index 4b0dbb728..8d5610678 100755 +--- a/install_python_deps.sh ++++ b/install_python_deps.sh +@@ -60,7 +60,7 @@ then + pip3 install -r tools/nntool/tests/requirements.txt + fi + +- pip3 install -r tools/audio-framework/requirements.txt ++# pip3 install -r tools/audio-framework/requirements.txt + pip3 install -r utils/gapy_v2/requirements.txt + pip3 install -r doc/requirements.txt + fi +diff --git a/utils/cmake/at.cmake b/utils/cmake/at.cmake +index 476b87c78..ee6449a1d 100644 +--- a/utils/cmake/at.cmake ++++ b/utils/cmake/at.cmake +@@ -909,7 +909,7 @@ macro(_at_model_setup_int) + OUTPUT ${PARAM_MODEL_EXE_PATH} + DEPENDS ${PARAM_MODEL_PATH} ${_MODEL_GEN_SRCS} ${TILER_LIB} + COMMAND ${CMAKE_COMMAND} -E make_directory ${_MODEL_EXE_DIR} +- COMMAND gcc -g -o ${PARAM_MODEL_EXE_PATH} ${_MODEL_GEN_INCS} ${PARAM_MODEL_PATH} ${_MODEL_GEN_SRCS} ${_MODEL_GEN_LIBS} ${SDL2_LDFLAGS} -lm ${PARAM_MODEL_EXTRA_COMPILE_FLAGS} ++ COMMAND x86_64-linux-gnu-gcc -g -o ${PARAM_MODEL_EXE_PATH} ${_MODEL_GEN_INCS} ${PARAM_MODEL_PATH} ${_MODEL_GEN_SRCS} ${_MODEL_GEN_LIBS} ${SDL2_LDFLAGS} -lm ${PARAM_MODEL_EXTRA_COMPILE_FLAGS} + ) + + # global compile model target +diff --git a/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py b/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py +index a7b191322..b0c5c5e77 100644 +--- a/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py ++++ b/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py +@@ -159,7 +159,7 @@ class Openocd(): + if self.args.openocd_tools is None: + raise RuntimeError("Argument --openocd-tools is missing") + +- def connect(self, use_existing_port=6666): ++ def connect(self, telnet_port=None, telnet_host='localhost'): + """Connect to target. + + This will launch OpenOCD with the telnet proxy and connect to it so that this class +@@ -169,10 +169,10 @@ class Openocd(): + # To allow the execution of several gapy in parallel, we cannot use a fixed port. + # Iterate until we find an available one. + retry_nb = 0 +- if use_existing_port is not None: ++ if telnet_port is not None: + success = True +- port = use_existing_port +- self.use_existing_port = use_existing_port ++ port = telnet_port ++ self.telnet_port = telnet_port + else: + success = False + while retry_nb < 30: +@@ -208,7 +208,7 @@ class Openocd(): + + # Now that OpenOCD was successfully launched with proxy openocd, launch a telnet. + if success is True: +- self.telnet = pexpect.spawn(f'telnet localhost {port}', encoding='utf-8', echo=False) ++ self.telnet = pexpect.spawn(f'telnet {telnet_host} {port}', encoding='utf-8', echo=False) + match = self.telnet.expect(['Open On-Chip Debugger'], timeout=None) + else: + raise RuntimeError('Failed to connect to openocd after 30 retries') +@@ -472,6 +472,12 @@ class Runner(): + parser.add_argument("--gdb-port", dest="gdb_port", default=3333, type=int, + help="GDB port") + ++ parser.add_argument("--telnet-port", dest="telnet_port", default=None, type=int, ++ help="Telnet port") ++ ++ parser.add_argument("--telnet-host", dest="telnet_host", default='localhost', type=str, ++ help="Telnet host") ++ + parser.add_argument("--wsl", dest = "wsl", type=str, default=None, + help = "Launch command in wsl environment") + +@@ -602,7 +608,7 @@ class Runner(): + + if ocd is None: + ocd = Openocd(args) +- ocd.connect() ++ ocd.connect(args.telnet_port, args.telnet_host) + + self.__flash_image(args, ocd, flash, base_addr, first_index, last) + +diff --git a/utils/openocd_tools/tcl/gap9revb.tcl b/utils/openocd_tools/tcl/gap9revb.tcl +index 4a66dfa27..dea57196b 100644 +--- a/utils/openocd_tools/tcl/gap9revb.tcl ++++ b/utils/openocd_tools/tcl/gap9revb.tcl +@@ -3,7 +3,7 @@ adapter_khz 5000 + + config_reset 0x1 + +-target create $_FC riscv -chain-position $_TAP_RISCV -coreid 0x9 ++target create $_FC riscv -chain-position $_TAP_RISCV -coreid 0x9 + + gdb_report_data_abort enable + gdb_report_register_access_error enable +@@ -23,8 +23,8 @@ proc jtag_init {} { + gap_reset 0 100 + + # wait for jtag ready +- poll_confreg 0x1 +- echo "INIT: confreg polling done" ++ # poll_confreg 0x1 ++ # echo "INIT: confreg polling done" + + $::_FC arp_examine + echo "examine done" +@@ -36,8 +36,8 @@ proc init_reset {mode} { + #targets $::_FC + gap_reset 1 100 + # wait for jtag ready +- poll_confreg 0x1 +- echo "RESET: confreg polling done" ++ # poll_confreg 0x1 ++ # echo "RESET: confreg polling done" + jtag arp_init + } + diff --git a/Container/gap9-sources.list b/Container/gap9-sources.list new file mode 100644 index 0000000000..afd5cb4726 --- /dev/null +++ b/Container/gap9-sources.list @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to +# newer versions of the distribution. +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted + +## Major bug fix updates produced after the final release of the +## distribution. +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted + +## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu +## team. Also, please note that software in universe WILL NOT receive any +## review or updates from the Ubuntu security team. +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy universe +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy universe +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates universe +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-updates universe + +## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu +## team, and may not be under a free licence. Please satisfy yourself as to +## your rights to use the software. Also, please note that software in +## multiverse WILL NOT receive any review or updates from the Ubuntu +## security team. +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy multiverse +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy multiverse +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates multiverse +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-updates multiverse + +## N.B. software from this repository may not have been tested as +## extensively as that contained in the main release, although it includes +## newer versions of some applications which may provide useful features. +## Also, please note that software in backports WILL NOT receive any review +## or updates from the Ubuntu security team. +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse + +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-security universe +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-security universe +deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-security multiverse +# deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-security multiverse diff --git a/Makefile b/Makefile index d40a49da11..075a716c62 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,9 @@ PICOLIBC_RV32IMA_INSTALL_DIR ?= ${LLVM_INSTALL_DIR}/picolibc/riscv/rv32ima PICOLIBC_RV32IMAFD_INSTALL_DIR ?= ${LLVM_INSTALL_DIR}/picolibc/riscv/rv32imafd PICOLIBC_RV32IMF_INSTALL_DIR ?= ${LLVM_INSTALL_DIR}/picolibc/riscv/rv32imf +GCC_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/gcc +GAP_RISCV_GCC_INSTALL_DIR ?= ${GCC_INSTALL_DIR}/gap9 + CHIMERA_SDK_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/chimera-sdk PULP_SDK_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/pulp-sdk SNITCH_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/snitch_cluster @@ -36,6 +39,7 @@ MINIMALLOC_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/minimalloc XTL_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/xtl XSIMD_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/xsimd XTENSOR_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/xtensor +GAP9_SDK_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/gap9-sdk CMAKE ?= cmake @@ -51,6 +55,9 @@ CHIMERA_SDK_COMMIT_HASH ?= b2392f6efcff75c03f4c65eaf3e12104442b22ea XTL_VERSION ?= 0.7.5 XSIMD_VERSION ?= 13.2.0 XTENSOR_VERSION ?= 0.25.0 +GAP9_SDK_COMMIT_HASH ?= 897955d7ab326bd31684429eb16a2e485ab89afb # dev-v5.19.2 +# GAP9_SDK_COMMIT_HASH ?= dfabdddd0e78b9b750a0eb46eee85d5a2c9ae853 # dev-v5.20.4 +GAP_SDK_URL ?= 'git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git' OS := $(shell uname -s) ARCH:= $(shell uname -m) @@ -77,6 +84,8 @@ echo-bash: @echo "The following symbols need to be exported for Deeploy to work properly:" @echo "export MINIMALLOC_INSTALL_DIR=${MINIMALLOC_INSTALL_DIR}" @echo "export PULP_SDK_HOME=${PULP_SDK_INSTALL_DIR}" + @echo "export GAP_SDK_HOME=${GAP9_SDK_INSTALL_DIR}" + @echo "export GAP_RISCV_GCC_TOOLCHAIN=${GAP_RISCV_GCC_INSTALL_DIR}" @echo "export CHIMERA_SDK_HOME=${CHIMERA_SDK_INSTALL_DIR}" @echo "export SNITCH_HOME=${SNITCH_INSTALL_DIR}" @echo "export GVSOC_INSTALL_DIR=${GVSOC_INSTALL_DIR}" @@ -97,9 +106,11 @@ emulators: snitch_runtime pulp-sdk qemu banshee mempool ${TOOLCHAIN_DIR}/llvm-project: cd ${TOOLCHAIN_DIR} && \ - git clone https://github.com/pulp-platform/llvm-project.git \ - -b main && \ - cd ${TOOLCHAIN_DIR}/llvm-project && git checkout ${LLVM_COMMIT_HASH} && \ + git init llvm-project && \ + cd ${TOOLCHAIN_DIR}/llvm-project && \ + git remote add origin https://github.com/pulp-platform/llvm-project.git && \ + git fetch --depth=1 origin ${LLVM_COMMIT_HASH} && \ + git checkout ${LLVM_COMMIT_HASH} && \ git submodule update --init --recursive ${LLVM_INSTALL_DIR}: ${TOOLCHAIN_DIR}/llvm-project @@ -410,6 +421,26 @@ ${PULP_SDK_INSTALL_DIR}: ${TOOLCHAIN_DIR}/pulp-sdk pulp-sdk: ${PULP_SDK_INSTALL_DIR} +${TOOLCHAIN_DIR}/gap9-toolchain: + cd ${TOOLCHAIN_DIR} && \ + git clone https://github.com/GreenWaves-Technologies/gap_riscv_toolchain_ubuntu.git --depth 1 -b master gap9-toolchain + +${GAP_RISCV_GCC_INSTALL_DIR}: ${TOOLCHAIN_DIR}/gap9-toolchain + cd ${TOOLCHAIN_DIR}/gap9-toolchain && \ + mkdir -p ${GAP_RISCV_GCC_INSTALL_DIR} && \ + ./install.sh ${GAP_RISCV_GCC_INSTALL_DIR} + +gap9-toolchain: ${GAP_RISCV_GCC_INSTALL_DIR} + +.PHONY: gap9-sdk +gap9-sdk: + @echo "Cloning and building GAP9 SDK..." + GAP9_SDK_INSTALL_DIR=${GAP9_SDK_INSTALL_DIR} \ + GAP9_SDK_COMMIT_HASH=${GAP9_SDK_COMMIT_HASH} \ + GAP_SDK_URL=${GAP_SDK_URL} \ + ROOT_DIR=${ROOT_DIR} \ + bash ${ROOT_DIR}/scripts/gap9-build_sdk.sh + ${TOOLCHAIN_DIR}/snitch_cluster: cd ${TOOLCHAIN_DIR} && \ git clone https://github.com/pulp-platform/snitch_cluster.git && \ diff --git a/README_GAP9.md b/README_GAP9.md new file mode 100644 index 0000000000..323465f617 --- /dev/null +++ b/README_GAP9.md @@ -0,0 +1,99 @@ +## Using Deeploy with GAP9 + +> ⚠️ **IMPORTANT NOTE** +> This is a work in progress. The GAP9 support in Deeploy is experimental and may not be fully functional. + +To use Deeploy with GAP9, a custom Docker container is required because the official Deeploy Docker image does yet not include the necessary SDKs and dependencies for GAP9 development, because they are not publicly available. + +### Build The Docker Container + +To use SSH keys for accessing private repositories during the Docker build process, make sure you have an SSH key pair set up on your local machine. By default, the Makefile uses the key located at `~/.ssh/id_ed25519`. If your key is located elsewhere, you can specify its path using the `SSH_PRIVATE_KEY` variable when invoking the make command. + +To build a local version of the Deeploy Docker image with GAP9 support using the upstream toolchain image, run: +```sh +cd Container + +# Build the Deeploy image with the upstream toolchain image +make deeploy-gap9 DEEPOY_GAP9_IMAGE=deeploy-gap9:latest + +# If you want to specify a custom SSH key path, use: +make deeploy-gap9 DEEPOY_GAP9_IMAGE=deeploy-gap9:latest SSH_PRIVATE_KEY=/path/to/your/private/key +``` + +Or, to build the toolchain, Deeploy and GAP9 images locally, use: +```sh +cd Container + +# To build the Deeploy container with the local toolchain image +make deeploy TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPOY_IMAGE=deeploy:gap9 + +# To build the Deeploy GAP9 container with the local toolchain image +make deeploy-gap9 TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPOY_IMAGE=deeploy:gap9 DEEPOY_GAP9_IMAGE=deeploy-gap9:latest +``` + +### Use The Docker Container + +Once the image is built, you can create and start the container in interactive mode with: + +```sh +docker run -it --name deeploy_gap9 -v $(pwd):/app/Deeploy deeploy-gap9:latest +``` + +Before running tests, you need to set up the GAP9 environment inside the container: +```sh +source /app/install/gap9-sdk/.gap9-venv/bin/activate +source /app/install/gap9-sdk/configs/gap9_evk_audio.sh +``` +Install Deeploy inside the container in editable mode: + +```sh +cd /app/Deeploy +pip install -e . --extra-index-url=https://pypi.ngc.nvidia.com +``` + +```sh +cd /app/Deeploy/DeeployTest +python deeployRunner_gap9.py -t ./Tests/Kernels/FP32/MatMul +python deeployRunner_tiled_gap9.py -t ./Tests/Kernels/FP32/MatMul +``` + +### Use A Real GAP9 Board (USB/IP via gap9-run.sh) + +For board access, use the orchestration script in [scripts/gap9-run.sh](scripts/gap9-run.sh). It manages: +- host-side USB/IP server (pyusbip) +- usbip device manager container +- GAP9 SDK container with the correct mounts + +#### Prerequisites +- Docker installed and running +- A working SSH key for BuildKit (if you are building the image locally) +- USB/IP host support (the script can set up pyusbip on the host) + + +#### Start the board workflow (recommended) +This launches a tmux session with two panes: one for the host USB/IP server and one for the GAP9 container. + +```sh +./scripts/gap9-run.sh start-tmux +``` + +#### Start manually (two terminals) +Terminal 1 (host USB/IP server): +```sh +./scripts/gap9-run.sh start-usbip-host +``` + +Terminal 2 (containers + attach device): +```sh +./scripts/gap9-run.sh start +``` + +#### Common options and environment variables +- Use a custom image: `-i your-gap9-image:tag` or `GAP9_IMAGE=your-gap9-image:tag` +- Set USB device IDs: `USBIP_VENDOR=15ba USBIP_PRODUCT=002b` +- Change USB/IP host: `USBIP_HOST=host.docker.internal` + +To stop everything: +```sh +./scripts/gap9-run.sh stop +``` diff --git a/scripts/gap9-build_sdk.sh b/scripts/gap9-build_sdk.sh new file mode 100755 index 0000000000..2df7bd3eeb --- /dev/null +++ b/scripts/gap9-build_sdk.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +# gap9-build_sdk.sh +# Helper script to clone, patch and build the GAP9 SDK. Intended to be +# invoked from the Makefile with environment variables set: +# GAP9_SDK_INSTALL_DIR (required) +# GAP9_SDK_COMMIT_HASH (optional, fallback provided) +# ROOT_DIR (optional, defaults to script dir) + +ROOT_DIR="${ROOT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +GAP9_SDK_INSTALL_DIR="${GAP9_SDK_INSTALL_DIR:?GAP9_SDK_INSTALL_DIR must be set}" +GAP9_SDK_COMMIT_HASH="${GAP9_SDK_COMMIT_HASH:-897955d7ab326bd31684429eb16a2e485ab89afb}" +GAP_SDK_URL="${GAP_SDK_URL:-'git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git'}" + +echo "Preparing GAP9 SDK in: ${GAP9_SDK_INSTALL_DIR}" + +if [ -d "${GAP9_SDK_INSTALL_DIR}/.git" ]; then + echo "Directory ${GAP9_SDK_INSTALL_DIR} already exists and looks like a git repo. Updating remote URL and fetching latest changes..." + git remote set-url origin "${GAP_SDK_URL}" || true +else + echo "Cloning GAP9 SDK..." + git clone "${GAP_SDK_URL}" "${GAP9_SDK_INSTALL_DIR}" +fi + +cd "${GAP9_SDK_INSTALL_DIR}" +echo "Checking out commit ${GAP9_SDK_COMMIT_HASH} (stash and fetch if necessary)" +git fetch --all --tags || true +git stash || true +git checkout "${GAP9_SDK_COMMIT_HASH}" +git submodule update --init --recursive + +# Platform specific patch +ARCH=$(dpkg --print-architecture 2>/dev/null || true) +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi +case "$ARCH" in +amd64 | x86_64) PATCH=gap9-amd64.patch ;; +arm64 | aarch64) PATCH=gap9-arm64.patch ;; +*) PATCH= ;; +esac + +set -e # Enable strict error handling for the build process +if [ -n "$PATCH" ] && [ -f "${ROOT_DIR}/${PATCH}" ]; then + echo "Applying platform patch: $PATCH" + git apply "${ROOT_DIR}/${PATCH}" +else + echo "No platform-specific patch to apply for architecture '$ARCH' (looked for ${ROOT_DIR}/${PATCH})" +fi +set +e # Disable strict error handling to allow deactivation even if build fails + +echo "Setting up Python virtual environment and installing dependencies" +python -m venv .gap9-venv +. .gap9-venv/bin/activate +pip install "numpy<2.0.0" +echo "Sourcing GAP9 SDK environment" +. configs/gap9_evk_audio.sh || true + +echo "Invoking make install_dependency cmake_sdk.build" +set -e # Enable strict error handling for the build process +make install_dependency cmake_sdk.build openocd.all +set +e # Disable strict error handling to allow deactivation even if build fails + +deactivate + +echo "GAP9 SDK ready at: ${GAP9_SDK_INSTALL_DIR}" + +exit 0 diff --git a/scripts/gap9-run.sh b/scripts/gap9-run.sh new file mode 100755 index 0000000000..912f062819 --- /dev/null +++ b/scripts/gap9-run.sh @@ -0,0 +1,543 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +# gap9-run.sh - Docker orchestration script for GAP9 SDK with USB/IP device passthrough +# +# +# This script manages the containerized GAP9 development environment, including: +# - Building the GAP9 Docker image +# - Running the usbip device manager container (for USB device passthrough) +# - Starting the GAP9 SDK container with mounted volumes +# +# Prerequisites: +# - Docker installed and running +# - SSH key configured for BuildKit (if building image) +# - pyusbip server running on host (for USB passthrough) +# + +set -euo pipefail + +######################################################################### +# Configuration & Defaults +######################################################################### + +# Image and container names +GAP9_IMAGE="ghcr.io/pulp-platform/deeploy-gap9" +USBIP_IMAGE="jonathanberi/devmgr" + +DOCKER_PLATFORM="auto" +DOCKER_SHELL="/bin/zsh" + +# USB/IP device settings +USBIP_HOST="host.docker.internal" +USBIP_VENDOR="15ba" +USBIP_PRODUCT="002b" + +# SDK and cache directories +WORK_DIR="." +CACHE_FOLDER=".cache" + +# SSH key gap9 container +SSH_PRIVATE_KEY="~/.ssh/id_ed25519" + +# pyusbip configuration +PYUSBIP_REPO="https://github.com/tumayt/pyusbip" +PYUSBIP_DIR=".pyusbip" + +######################################################################### +# Utility Functions +######################################################################### + +# Print colored output +log_info() { + echo -e "\033[0;36m[INFO ]\033[0m $*" +} + +log_error() { + echo -e "\033[0;31m[ERROR ]\033[0m $*" >&2 +} + +log_warn() { + echo -e "\033[0;33m[WARN ]\033[0m $*" >&2 +} + +log_success() { + echo -e "\033[0;32m[SUCCESS]\033[0m $*" +} + +# Display help message +show_help() { + cat < [options] + +GAP9 Docker Orchestration Script + +Commands: + start Start usbip daemon and GAP9 container + start-tmux Start everything in a tmux session with split panes + stop Stop containers + start-usbip-host Setup and run host-side USB/IP server (in separate terminal) + start-gap9 Start only the GAP9 container + start-usbip-daemon Start only the usbip device manager container + attach-usbip Attach USB device to usbip daemon + detach-usbip Detach USB device from usbip daemon + setup-usbip-host One-time setup for host-side USB/IP server + help Display this help message + +Options: + -i, --image NAME Docker image name (default: $GAP9_IMAGE) + -d, --work-dir PATH Path to working directory (default: $WORK_DIR) + -c, --cache-dir PATH Cache directory (default: $CACHE_FOLDER) + -k, --ssh-key PATH SSH private key (default: $SSH_PRIVATE_KEY) + -h, --host ADDR usbip host address (default: $USBIP_HOST) + -v, --vendor ID USB vendor ID (default: $USBIP_VENDOR) + -p, --product ID USB product ID (default: $USBIP_PRODUCT) + --platform PLATFORM Docker platform (default: $DOCKER_PLATFORM) + --shell SHELL Shell to use in container (default: $DOCKER_SHELL) + +Examples: + # Start everything in tmux (recommended) + $0 start-tmux + + # Start containers with USB device passthrough (manual terminals) + $0 start-usbip-host # In terminal 1 + $0 start # In terminal 2 + + # Custom working directory + $0 -d /path/to/workdir start + + # Stop everything + $0 stop + +EOF +} + +# Parse command-line arguments (collect options first, then run command) +command="" +opts=() +args=("$@") +idx=0 +while [[ $idx -lt ${#args[@]} ]]; do + case "${args[$idx]}" in + -i | --image) + GAP9_IMAGE="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -d | --work-dir) + WORK_DIR="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -c | --cache-dir) + CACHE_FOLDER="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -k | --ssh-key) + SSH_PRIVATE_KEY="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -h | --host) + USBIP_HOST="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -v | --vendor) + USBIP_VENDOR="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -p | --product) + USBIP_PRODUCT="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + --platform) + DOCKER_PLATFORM="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + --shell) + DOCKER_SHELL="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + help | --help) + show_help + exit 0 + ;; + start | start-tmux | stop | start-gap9 | start-usbip-daemon | attach-usbip | detach-usbip | stop-usbip-daemon | start-usbip-host | setup-usbip-host) + if [[ -n "$command" ]]; then + log_error "Multiple commands provided: $command and ${args[$idx]}" + show_help + exit 1 + fi + command="${args[$idx]}" + idx=$((idx + 1)) + ;; + *) + log_error "Unknown option or command: ${args[$idx]}" + show_help + exit 1 + ;; + esac +done + +# Expand path of SSH private key +SSH_PRIVATE_KEY="$(eval echo "$SSH_PRIVATE_KEY")" + +## Print configuration +log_info "Configuration:" +log_info " GAP9 Docker Image: $GAP9_IMAGE" +log_info " Working Directory: $WORK_DIR" +log_info " Cache Directory: $CACHE_FOLDER" +log_info " SSH Private Key: $SSH_PRIVATE_KEY" +log_info " USB/IP Host: $USBIP_HOST" +log_info " USB Vendor ID: $USBIP_VENDOR" +log_info " USB Product ID: $USBIP_PRODUCT" +log_info " Docker Platform: $DOCKER_PLATFORM" +log_info " Docker Shell: $DOCKER_SHELL" + +######################################################################### +# usbip Host Setup Functions +######################################################################### + +cmd_setup_usbip_host() { + log_info "Setting up host-side USB/IP server..." + + # Clone pyusbip if not present + if [ ! -d "$PYUSBIP_DIR" ]; then + log_info "Cloning pyusbip into $PYUSBIP_DIR..." + git clone "$PYUSBIP_REPO" "$PYUSBIP_DIR" + else + log_info "pyusbip directory $PYUSBIP_DIR already exists, skipping clone" + fi + + # Create virtual environment if needed + if [ ! -d "$PYUSBIP_DIR/.venv" ]; then + log_info "Creating Python virtual environment..." + python3 -m venv "$PYUSBIP_DIR/.venv" + + log_info "Installing pyusbip dependencies..." + # shellcheck disable=SC1091 + . "$PYUSBIP_DIR/.venv/bin/activate" + pip install --upgrade pip + pip install libusb1 + else + log_info "Virtual environment already exists, skipping setup" + fi + + log_success "Host-side USB/IP setup complete" +} + +cmd_start_usbip_host() { + cmd_setup_usbip_host + + log_info "Starting host-side USB/IP server (pyusbip)..." + log_info "This process will run in the foreground. Press Ctrl+C to stop." + + cd "$PYUSBIP_DIR" && + # shellcheck disable=SC1091 + . .venv/bin/activate && + python pyusbip.py +} + +######################################################################### +# usbip Daemon Functions +######################################################################### + +# Wait for usbip server to be ready +wait_for_usbip_server() { + local max_retries=20 + local retry_count=0 + local retry_interval=1 + + log_info "Waiting for pyusbip server to be ready..." + + while [ $retry_count -lt $max_retries ]; do + if pgrep -f "python.*pyusbip\.py" >/dev/null; then + log_success "pyusbip server is ready" + return 0 + fi + + retry_count=$((retry_count + 1)) + log_info " Attempt $retry_count/$max_retries: pyusbip not ready yet, retrying in ${retry_interval}s..." + sleep "$retry_interval" + done + + log_error "Timeout waiting for pyusbip server to start (${max_retries}s)" + return 1 +} + +cmd_start_usbip_daemon() { + # Wait for pyusbip server to be ready + if ! wait_for_usbip_server; then + log_error "pyusbip server did not start in time" + log_error "Please run '$0 start-usbip-host' in a separate terminal first" + exit 1 + fi + + # Check if container already running + if [ -n "$(docker ps -q -f name=usbip-devmgr)" ]; then + log_info "usbip-devmgr container already running" + return 0 + fi + + log_info "Starting usbip-devmgr container..." + docker run -d --rm \ + --privileged \ + --pid=host \ + --name usbip-devmgr \ + -e USBIP_HOST="$USBIP_HOST" \ + -e USBIP_VENDOR="$USBIP_VENDOR" \ + -e USBIP_PRODUCT="$USBIP_PRODUCT" \ + "$USBIP_IMAGE" \ + /bin/sh -lc 'nsenter -t1 -m sh -lc "tail -f /dev/null"' + + log_success "usbip-devmgr container started" +} + +cmd_attach_usbip() { + # First detach any existing attachment + cmd_detach_usbip || true + + log_info "Attaching USB device to usbip-devmgr..." + docker exec -it usbip-devmgr /bin/sh -lc 'nsenter -t1 -m sh -lc " + usbip list -r \"$USBIP_HOST\" || { echo \"usbip list failed\"; exit 1; } + BUSID=\$(usbip list -r \"$USBIP_HOST\" \ + | grep \"$USBIP_VENDOR:$USBIP_PRODUCT\" \ + | head -n1 \ + | cut -d\":\" -f1 \ + | xargs) + if [ -z \"\$BUSID\" ]; then + exit 1 + fi + usbip attach -r \"$USBIP_HOST\" -b \"\$BUSID\" + "' + + if [ $? -ne 0 ]; then + log_error "Failed to attach USB device" + else + log_success "USB device attached successfully" + fi +} + +cmd_detach_usbip() { + log_info "Detaching USB device..." + docker run --rm \ + --privileged \ + --pid=host \ + -e USBIP_HOST="$USBIP_HOST" \ + -e USBIP_VENDOR="$USBIP_VENDOR" \ + -e USBIP_PRODUCT="$USBIP_PRODUCT" \ + "$USBIP_IMAGE" \ + /bin/sh -lc 'nsenter -t1 -m sh -lc " + PORT=\$(usbip port \ + | grep \"$USBIP_VENDOR:$USBIP_PRODUCT\" -B 1 \ + | head -n1 \ + | sed -E \"s/^Port ([0-9]+):.*/\1/\" \ + | xargs) + if [ -z \"\$PORT\" ]; then + exit 1 + fi + usbip detach -p \"\$PORT\" + "' >/dev/null 2>&1 + + if [ $? -ne 0 ]; then + log_warn "Failed to detach USB device (it may not have been attached)" + else + log_success "USB device detached (or not attached)" + fi +} + +cmd_stop_usbip_daemon() { + log_info "Stopping usbip-devmgr container..." + + # First detach the device + cmd_detach_usbip || true + + # Stop the container + if [ -n "$(docker ps -q -f name=usbip-devmgr)" ]; then + docker stop usbip-devmgr >/dev/null 2>&1 + log_success "usbip-devmgr container stopped" + else + log_info "usbip-devmgr container not running" + fi +} + +######################################################################### +# GAP9 Container Functions +######################################################################### + +cmd_start_gap9() { + log_info "Starting GAP9 container..." + + # Prepare cache directory + mkdir -p "$CACHE_FOLDER" + touch "$CACHE_FOLDER/.zsh_history" + + # Validate WORK_DIR exists + if [ ! -d "$WORK_DIR" ]; then + log_error "WORK_DIR not found: $WORK_DIR" + log_error "Use -d/--work-dir to set the SDK path" + exit 1 + fi + + log_info "Press Ctrl+D or type 'exit' to exit container" + + # Build docker run command with optional platform argument + local docker_run_args=( + -it --rm + --privileged + -v /dev/bus/usb:/dev/bus/usb + -v "$SSH_PRIVATE_KEY":/root/.ssh/id_ed25519:ro + -v "$WORK_DIR/":/app/work/ + -v "$CACHE_FOLDER/.zsh_history":/root/.zsh_history + -v "$CACHE_FOLDER/ccache":/ccache + -e CCACHE_DIR=/ccache + ) + + # Add platform argument if not 'auto' + if [[ "$DOCKER_PLATFORM" != "auto" ]]; then + docker_run_args+=(--platform "$DOCKER_PLATFORM") + fi + + docker_run_args+=("$GAP9_IMAGE" "$DOCKER_SHELL" -c "cd /app/work && $DOCKER_SHELL") + + docker run "${docker_run_args[@]}" +} + +######################################################################### +# Orchestration Functions +######################################################################### + +cmd_start() { + log_info "Starting GAP9 orchestration (usbip daemon + GAP9 container)..." + cmd_start_usbip_daemon + cmd_attach_usbip + cmd_start_gap9 +} + +cmd_stop() { + log_info "Stopping all containers..." + cmd_stop_usbip_daemon + cmd_stop_tmux + log_success "All containers stopped" +} + +######################################################################### +# Tmux Orchestration +######################################################################### + +cmd_start_tmux() { + local session_name="gap9-dev" + local script_path="$0" + local opts_escaped="" + + for opt in "${opts[@]:-}"; do + if [[ -n "$opt" ]]; then + printf -v opt '%q' "$opt" + opts_escaped+=" $opt" + fi + done + + # Check if tmux is installed + if ! command -v tmux &>/dev/null; then + log_error "tmux is not installed. Please install tmux first." + log_error "On macOS: brew install tmux" + log_error "On Linux: sudo apt-get install tmux" + exit 1 + fi + + # Kill any existing session with the same name + tmux kill-session -t "$session_name" 2>/dev/null || true + + log_info "Creating tmux session: $session_name" + + # Create new session with three panes (usbip-host, usbip-daemon, gap9) + tmux new-session -d -s "$session_name" -x 200 -y 50 + + # First pane: run pyusbip server + + # Second pane: run main orchestration (with delay to let server start) + tmux split-window -t "$session_name:0" -h + tmux split-window -t "$session_name:0" -v + tmux send-keys -t "$session_name:0.1" "alias stop='$script_path$opts_escaped stop'" Enter + tmux send-keys -t "$session_name:0.1" "$script_path$opts_escaped start-usbip-host" Enter + tmux send-keys -t "$session_name:0.2" "alias stop='$script_path$opts_escaped stop'" Enter + tmux send-keys -t "$session_name:0.2" "$script_path$opts_escaped start-usbip-daemon" Enter + tmux send-keys -t "$session_name:0.2" "$script_path$opts_escaped attach-usbip" Enter + tmux send-keys -t "$session_name:0.0" "alias stop='$script_path$opts_escaped stop'" Enter + tmux send-keys -t "$session_name:0.0" "$script_path$opts_escaped start-gap9" Enter + + # Select the first pane + tmux select-pane -t "$session_name:0.0" + + log_success "tmux session created: $session_name" + log_info "Attaching to session..." + log_info "To detach: Ctrl+B then D" + log_info "To kill session: tmux kill-session -t $session_name" + + # Attach to the session + tmux attach-session -t "$session_name" +} + +cmd_stop_tmux() { + local session_name="gap9-dev" + log_info "Stopping tmux session: $session_name" + tmux kill-session -t "$session_name" 2>/dev/null || log_info "tmux session $session_name not running" +} + +######################################################################### +# Main Script Logic +######################################################################### + +# If no command provided, show help +if [[ -z "$command" ]]; then + cmd_start_tmux + exit 0 +fi + +# Execute the command after options are parsed +case "$command" in +start) + cmd_start + ;; +start-tmux) + cmd_start_tmux + ;; +stop) + cmd_stop + ;; +start-gap9) + cmd_start_gap9 + ;; +start-usbip-daemon) + cmd_start_usbip_daemon + ;; +attach-usbip) + cmd_attach_usbip + ;; +detach-usbip) + cmd_detach_usbip + ;; +stop-usbip-daemon) + cmd_stop_usbip_daemon + ;; +start-usbip-host) + cmd_start_usbip_host + ;; +setup-usbip-host) + cmd_setup_usbip_host + ;; +*) + log_error "Unknown command: $command" + show_help + exit 1 + ;; +esac From 97c2d2b6aa89a116de1458ba3d67e908d477a6b2 Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Wed, 4 Feb 2026 22:30:06 +0100 Subject: [PATCH 06/79] Fix spelling mistakes and remove dependencies from fork --- .github/workflows/docker-build-deeploy-gap9.yml | 2 +- .github/workflows/docker-build-deeploy.yml | 2 +- Container/Makefile | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-deeploy-gap9.yml b/.github/workflows/docker-build-deeploy-gap9.yml index e6a9ab7016..76e7c6b2fa 100644 --- a/.github/workflows/docker-build-deeploy-gap9.yml +++ b/.github/workflows/docker-build-deeploy-gap9.yml @@ -96,7 +96,7 @@ jobs: env: OWNER: "${{ github.repository_owner }}" - - name: Build and push final deploy image + - name: Build and push final Deeploy image id: build uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/docker-build-deeploy.yml b/.github/workflows/docker-build-deeploy.yml index a4d0221e1f..ff2b507405 100644 --- a/.github/workflows/docker-build-deeploy.yml +++ b/.github/workflows/docker-build-deeploy.yml @@ -109,7 +109,7 @@ jobs: env: OWNER: "${{ github.repository_owner }}" - - name: Build and push final deploy image + - name: Build and push final Deeploy image id: build uses: docker/build-push-action@v6 with: diff --git a/Container/Makefile b/Container/Makefile index b1da56190b..e38d99c519 100644 --- a/Container/Makefile +++ b/Container/Makefile @@ -40,7 +40,7 @@ toolchain: -f Dockerfile.toolchain \ -t $(TOOLCHAIN_IMAGE) .. -# Build the final deploy image using the toolchain image +# Build the final Deeploy image using the toolchain image deeploy: DOCKER_BUILDKIT=1 docker build \ --ssh default \ @@ -48,7 +48,7 @@ deeploy: --build-arg BASE_IMAGE=$(TOOLCHAIN_IMAGE) \ -t $(DEEPOY_IMAGE) .. -# Build the GAP9 deploy image +# Build the GAP9 Deeploy image deeploy-gap9: eval $$(ssh-agent) && \ ssh-add $(SSH_PRIVATE_KEY) && \ From 3423c546f2ca6efff476a2bc94be861a72a546f6 Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Fri, 6 Feb 2026 17:23:50 +0100 Subject: [PATCH 07/79] Fix Missing Version Link --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 6ca3d33c6f..88b86d601c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -70,6 +70,7 @@ ["main", "https://pulp-platform.github.io/Deeploy"], ["devel", "https://pulp-platform.github.io/Deeploy/branch/devel"], ["v0.2.0", "https://pulp-platform.github.io/Deeploy/tag/v0.2.0"], + ["v0.2.1", "https://pulp-platform.github.io/Deeploy/tag/v0.2.1"], ], } From d6b6ac962f8dcda79bf3299c35b8a4bc831c9b90 Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Sun, 8 Feb 2026 22:28:08 +0100 Subject: [PATCH 08/79] Temporarily disable GAP9 on forks --- .github/workflows/ci-platform-gap9-tiled.yml | 1 + .github/workflows/ci-platform-gap9.yml | 1 + .github/workflows/infra-generate-ccache-gap9.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci-platform-gap9-tiled.yml b/.github/workflows/ci-platform-gap9-tiled.yml index 0043f8d3e9..4a3afc717b 100644 --- a/.github/workflows/ci-platform-gap9-tiled.yml +++ b/.github/workflows/ci-platform-gap9-tiled.yml @@ -25,6 +25,7 @@ concurrency: jobs: select-env: + if: github.repository == 'pulp-platform/Deeploy' uses: ./.github/workflows/_select-env.yml with: docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || github.repository == 'pulp-platform/Deeploy' && 'ghcr.io/pulp-platform/deeploy-gap9:latest'}} diff --git a/.github/workflows/ci-platform-gap9.yml b/.github/workflows/ci-platform-gap9.yml index 079f13c2a5..e9a59b4927 100644 --- a/.github/workflows/ci-platform-gap9.yml +++ b/.github/workflows/ci-platform-gap9.yml @@ -26,6 +26,7 @@ concurrency: jobs: select-env: + if: github.repository == 'pulp-platform/Deeploy' uses: ./.github/workflows/_select-env.yml with: docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || (github.repository == 'pulp-platform/Deeploy' && 'ghcr.io/pulp-platform/deeploy-gap9:latest') }} diff --git a/.github/workflows/infra-generate-ccache-gap9.yml b/.github/workflows/infra-generate-ccache-gap9.yml index b189bfd708..b54f43ce4c 100644 --- a/.github/workflows/infra-generate-ccache-gap9.yml +++ b/.github/workflows/infra-generate-ccache-gap9.yml @@ -18,6 +18,7 @@ name: Infrastructure • Generate CCache GAP9 jobs: generate-ccache-gap9: + if: github.repository == 'pulp-platform/Deeploy' runs-on: ubuntu-latest container: image: ${{ github.event.inputs.docker_image_deeploy || 'ghcr.io/pulp-platform/deeploy-gap9:latest' }} From daf8cdabff148194723ee253a5fde6f9df893557 Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Thu, 12 Feb 2026 12:33:37 +0100 Subject: [PATCH 09/79] Add Shell Format pre-commit --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6f07fbc8d..a8f323faf5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,3 +72,7 @@ repos: - id: yamllint name: Lint YAML Files stages: [pre-commit, pre-merge-commit, pre-push, manual] + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.12.0-2 + hooks: + - id: shfmt From f1c7d5757c127ec506fef9ab9e87c946bb07a38b Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Thu, 12 Feb 2026 19:07:52 +0100 Subject: [PATCH 10/79] Update to GAP9 SDK `v5.21.1-staging-1` --- Container/Dockerfile.deeploy-gap9 | 4 +- Container/gap9-amd64.patch | 74 ++++--- Container/gap9-arm64.patch | 338 ++++-------------------------- Makefile | 7 +- 4 files changed, 92 insertions(+), 331 deletions(-) diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 index 5232636d0c..05d3993461 100644 --- a/Container/Dockerfile.deeploy-gap9 +++ b/Container/Dockerfile.deeploy-gap9 @@ -68,7 +68,9 @@ RUN apt-get update && \ telnet \ usbutils \ libftdi-dev \ - libusb-1.0-0-dev && \ + libusb-1.0-0-dev \ + bison \ + flex && \ rm -rf /var/lib/apt/lists/* diff --git a/Container/gap9-amd64.patch b/Container/gap9-amd64.patch index 64c4a2421f..ef90f76938 100644 --- a/Container/gap9-amd64.patch +++ b/Container/gap9-amd64.patch @@ -1,38 +1,62 @@ diff --git a/.gitignore b/.gitignore -index d6c5b54d8..170acc552 100644 +index 6e751e203..d9091f1b1 100644 --- a/.gitignore +++ b/.gitignore -@@ -7,6 +7,7 @@ build - BUILD* - junit-reports/ - reports/ +@@ -57,3 +57,5 @@ gaptest_reports/ + # Release script dry-run output + release_dry_run/ + artifact.txt ++ +.gap9-venv - - - # SDK generated folders diff --git a/install_python_deps.sh b/install_python_deps.sh -index 4b0dbb728..8d5610678 100755 +index cac3e1885..578bb5f71 100755 --- a/install_python_deps.sh +++ b/install_python_deps.sh -@@ -60,7 +60,7 @@ then - pip3 install -r tools/nntool/tests/requirements.txt +@@ -69,7 +69,7 @@ then + python -m pip install -r tools/nntool/tests/requirements.txt fi - -- pip3 install -r tools/audio-framework/requirements.txt -+# pip3 install -r tools/audio-framework/requirements.txt - pip3 install -r utils/gapy_v2/requirements.txt - pip3 install -r doc/requirements.txt - fi + +- python -m pip install -r tools/audio-framework/requirements.txt ++ # python -m pip install -r tools/audio-framework/requirements.txt + python -m pip install -r utils/gapy_v2/requirements.txt + python -m pip install -r doc/requirements.txt + diff --git a/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py b/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py -index a7b191322..4adaede16 100644 +index 62ee5cac2..d872073f7 100644 --- a/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py +++ b/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py -@@ -208,7 +208,7 @@ class Openocd(): - - # Now that OpenOCD was successfully launched with proxy openocd, launch a telnet. - if success is True: -- self.telnet = pexpect.spawn(f'telnet localhost {port}', encoding='utf-8', echo=False) -+ self.telnet = pexpect.spawn(f'telnet host.docker.internal {port}', encoding='utf-8', echo=False) - match = self.telnet.expect(['Open On-Chip Debugger'], timeout=None) +@@ -342,7 +342,7 @@ class Openocd(): else: raise RuntimeError('Failed to connect to openocd after 30 retries') + +- self.telnet = pexpect.spawn(f'telnet localhost {port}', encoding='utf-8', echo=False) ++ self.telnet = pexpect.spawn(f'telnet host.docker.internal {port}', encoding='utf-8', echo=False) + match = self.telnet.expect(['Open On-Chip Debugger'], timeout=None) + + def write(self, addr: int, value: int): +diff --git a/utils/openocd_tools/tcl/gap9revb.tcl b/utils/openocd_tools/tcl/gap9revb.tcl +index 9e67f67c7..697483112 100644 +--- a/utils/openocd_tools/tcl/gap9revb.tcl ++++ b/utils/openocd_tools/tcl/gap9revb.tcl +@@ -24,8 +24,8 @@ proc jtag_init {} { + #gap_reset 0 100 + + # wait for jtag ready +- poll_confreg 0x1 +- echo "INIT: confreg polling done" ++ # poll_confreg 0x1 ++ # echo "INIT: confreg polling done" + + #UNCOMMENT IF YOU UNCOMMENT gap_init 1 + #gap_start 1 +@@ -39,8 +39,8 @@ proc init_reset {mode} { + #targets $::_FC + gap_reset 1 100 + # wait for jtag ready +- poll_confreg 0x1 +- echo "RESET: confreg polling done" ++ # poll_confreg 0x1 ++ # echo "RESET: confreg polling done" + jtag arp_init + } + diff --git a/Container/gap9-arm64.patch b/Container/gap9-arm64.patch index 6d2933627c..d5ccc643b1 100644 --- a/Container/gap9-arm64.patch +++ b/Container/gap9-arm64.patch @@ -1,20 +1,18 @@ diff --git a/.gitignore b/.gitignore -index d6c5b54d8..170acc552 100644 +index 6e751e203..d9091f1b1 100644 --- a/.gitignore +++ b/.gitignore -@@ -7,6 +7,7 @@ build - BUILD* - junit-reports/ - reports/ +@@ -57,3 +57,5 @@ gaptest_reports/ + # Release script dry-run output + release_dry_run/ + artifact.txt ++ +.gap9-venv - - - # SDK generated folders diff --git a/Makefile b/Makefile -index b342800d6..54a5d70c5 100644 +index 1dbab3578..60467d1d6 100644 --- a/Makefile +++ b/Makefile -@@ -131,7 +131,7 @@ openocd.checkout: +@@ -119,7 +119,7 @@ openocd.checkout: fi openocd.build: openocd.checkout @@ -24,245 +22,33 @@ index b342800d6..54a5d70c5 100644 openocd.checkout.gap9.5: diff --git a/configs/common.sh b/configs/common.sh -index e594437e3..b7ba06a63 100644 +index bd1f6ffea..e63faefa0 100644 --- a/configs/common.sh +++ b/configs/common.sh -@@ -80,7 +80,7 @@ export PULP_CONFIGS_PATH=$INSTALL_DIR/configs - export GVSOC_MODULES="${GAP_SDK_HOME}/gvsoc/gvsoc/models;${GAP_SDK_HOME}/gvsoc/gvsoc_gap" - export PYTHONPATH="${GAP_SDK_HOME}/gvsoc/gvsoc_gap:$PYTHONPATH" - if [ -d "$GAP_SDK_HOME/gvsoc/gvsoc_libs" ]; then -- export CONFIG_GVSOC_SKIP_UDMA_BUILD=1 -+ export CONFIG_GVSOC_SKIP_UDMA_BUILD=0 +@@ -178,3 +178,5 @@ if [ -n "$GAP_SDK_CI_BUILD" -a -z "$GAP_SDK_CI_DISABLE_GITHUB_ENV" ]; then fi - - # gaptest -diff --git a/gvsoc/gvsoc_gap/gap/gap9/soc.py b/gvsoc/gvsoc_gap/gap/gap9/soc.py -index e2877a2f9..e3fa337f0 100644 ---- a/gvsoc/gvsoc_gap/gap/gap9/soc.py -+++ b/gvsoc/gvsoc_gap/gap/gap9/soc.py -@@ -440,22 +440,22 @@ class Soc(st.Component): - self.bind(udma, 'i2s1_ws_out', udma, 'sfu_ws_in_1') - self.bind(udma, 'i2s2_ws_out', udma, 'sfu_ws_in_2') - -- self.bind(udma, 'sfu_stream_in_ready_0', udma, 'stream_in_ready_0') -- self.bind(udma, 'sfu_stream_in_ready_1', udma, 'stream_in_ready_1') -- self.bind(udma, 'sfu_stream_in_ready_2', udma, 'stream_in_ready_2') -- self.bind(udma, 'sfu_stream_in_ready_3', udma, 'stream_in_ready_3') -- self.bind(udma, 'sfu_stream_in_ready_4', udma, 'stream_in_ready_4') -- self.bind(udma, 'sfu_stream_in_ready_5', udma, 'stream_in_ready_5') -- self.bind(udma, 'sfu_stream_in_ready_6', udma, 'stream_in_ready_6') -- self.bind(udma, 'sfu_stream_in_ready_7', udma, 'stream_in_ready_7') -- self.bind(udma, 'sfu_stream_in_ready_8', udma, 'stream_in_ready_8') -- self.bind(udma, 'sfu_stream_in_ready_9', udma, 'stream_in_ready_9') -- self.bind(udma, 'sfu_stream_in_ready_10', udma, 'stream_in_ready_10') -- self.bind(udma, 'sfu_stream_in_ready_11', udma, 'stream_in_ready_11') -- self.bind(udma, 'sfu_stream_in_ready_12', udma, 'stream_in_ready_12') -- self.bind(udma, 'sfu_stream_in_ready_13', udma, 'stream_in_ready_13') -- self.bind(udma, 'sfu_stream_in_ready_14', udma, 'stream_in_ready_14') -- self.bind(udma, 'sfu_stream_in_ready_15', udma, 'stream_in_ready_15') -+ # self.bind(udma, 'sfu_stream_in_ready_0', udma, 'stream_in_ready_0') -+ # self.bind(udma, 'sfu_stream_in_ready_1', udma, 'stream_in_ready_1') -+ # self.bind(udma, 'sfu_stream_in_ready_2', udma, 'stream_in_ready_2') -+ # self.bind(udma, 'sfu_stream_in_ready_3', udma, 'stream_in_ready_3') -+ # self.bind(udma, 'sfu_stream_in_ready_4', udma, 'stream_in_ready_4') -+ # self.bind(udma, 'sfu_stream_in_ready_5', udma, 'stream_in_ready_5') -+ # self.bind(udma, 'sfu_stream_in_ready_6', udma, 'stream_in_ready_6') -+ # self.bind(udma, 'sfu_stream_in_ready_7', udma, 'stream_in_ready_7') -+ # self.bind(udma, 'sfu_stream_in_ready_8', udma, 'stream_in_ready_8') -+ # self.bind(udma, 'sfu_stream_in_ready_9', udma, 'stream_in_ready_9') -+ # self.bind(udma, 'sfu_stream_in_ready_10', udma, 'stream_in_ready_10') -+ # self.bind(udma, 'sfu_stream_in_ready_11', udma, 'stream_in_ready_11') -+ # self.bind(udma, 'sfu_stream_in_ready_12', udma, 'stream_in_ready_12') -+ # self.bind(udma, 'sfu_stream_in_ready_13', udma, 'stream_in_ready_13') -+ # self.bind(udma, 'sfu_stream_in_ready_14', udma, 'stream_in_ready_14') -+ # self.bind(udma, 'sfu_stream_in_ready_15', udma, 'stream_in_ready_15') - - self.bind(udma, 'stream_in_data_0', udma, 'sfu_stream_in_data_0') - self.bind(udma, 'stream_in_data_1', udma, 'sfu_stream_in_data_1') -@@ -491,22 +491,22 @@ class Soc(st.Component): - self.bind(udma, 'stream_out_ready_14', udma, 'sfu_stream_out_ready_14') - self.bind(udma, 'stream_out_ready_15', udma, 'sfu_stream_out_ready_15') - -- self.bind(udma, 'sfu_stream_out_data_0', udma, 'stream_out_data_0') -- self.bind(udma, 'sfu_stream_out_data_1', udma, 'stream_out_data_1') -- self.bind(udma, 'sfu_stream_out_data_2', udma, 'stream_out_data_2') -- self.bind(udma, 'sfu_stream_out_data_3', udma, 'stream_out_data_3') -- self.bind(udma, 'sfu_stream_out_data_4', udma, 'stream_out_data_4') -- self.bind(udma, 'sfu_stream_out_data_5', udma, 'stream_out_data_5') -- self.bind(udma, 'sfu_stream_out_data_6', udma, 'stream_out_data_6') -- self.bind(udma, 'sfu_stream_out_data_7', udma, 'stream_out_data_7') -- self.bind(udma, 'sfu_stream_out_data_8', udma, 'stream_out_data_8') -- self.bind(udma, 'sfu_stream_out_data_9', udma, 'stream_out_data_9') -- self.bind(udma, 'sfu_stream_out_data_10', udma, 'stream_out_data_10') -- self.bind(udma, 'sfu_stream_out_data_11', udma, 'stream_out_data_11') -- self.bind(udma, 'sfu_stream_out_data_12', udma, 'stream_out_data_12') -- self.bind(udma, 'sfu_stream_out_data_13', udma, 'stream_out_data_13') -- self.bind(udma, 'sfu_stream_out_data_14', udma, 'stream_out_data_14') -- self.bind(udma, 'sfu_stream_out_data_15', udma, 'stream_out_data_15') -+ # self.bind(udma, 'sfu_stream_out_data_0', udma, 'stream_out_data_0') -+ # self.bind(udma, 'sfu_stream_out_data_1', udma, 'stream_out_data_1') -+ # self.bind(udma, 'sfu_stream_out_data_2', udma, 'stream_out_data_2') -+ # self.bind(udma, 'sfu_stream_out_data_3', udma, 'stream_out_data_3') -+ # self.bind(udma, 'sfu_stream_out_data_4', udma, 'stream_out_data_4') -+ # self.bind(udma, 'sfu_stream_out_data_5', udma, 'stream_out_data_5') -+ # self.bind(udma, 'sfu_stream_out_data_6', udma, 'stream_out_data_6') -+ # self.bind(udma, 'sfu_stream_out_data_7', udma, 'stream_out_data_7') -+ # self.bind(udma, 'sfu_stream_out_data_8', udma, 'stream_out_data_8') -+ # self.bind(udma, 'sfu_stream_out_data_9', udma, 'stream_out_data_9') -+ # self.bind(udma, 'sfu_stream_out_data_10', udma, 'stream_out_data_10') -+ # self.bind(udma, 'sfu_stream_out_data_11', udma, 'stream_out_data_11') -+ # self.bind(udma, 'sfu_stream_out_data_12', udma, 'stream_out_data_12') -+ # self.bind(udma, 'sfu_stream_out_data_13', udma, 'stream_out_data_13') -+ # self.bind(udma, 'sfu_stream_out_data_14', udma, 'stream_out_data_14') -+ # self.bind(udma, 'sfu_stream_out_data_15', udma, 'stream_out_data_15') - - # Riscv bus watchpoint - if self.get_property('fc/riscv_fesvr_tohost_addr') is not None: -diff --git a/gvsoc/gvsoc_gap/gap/udma/CMakeLists.txt b/gvsoc/gvsoc_gap/gap/udma/CMakeLists.txt -index 00ea2162a..1abbe5c14 100644 ---- a/gvsoc/gvsoc_gap/gap/udma/CMakeLists.txt -+++ b/gvsoc/gvsoc_gap/gap/udma/CMakeLists.txt -@@ -21,6 +21,7 @@ vp_model(NAME "gap.udma.udma_v4_gap9_v2_impl" - "i2c/v4/udma_i2c_ucode.cpp" - "hyper/udma_hyper_v3.cpp" - "mram/udma_mram_v2.cpp" -+ "sfu/udma_sfu_v1_empty.cpp" - ) - - vp_model_compile_definitions(NAME "gap.udma.udma_v4_gap9_v2_impl" -@@ -34,6 +35,7 @@ vp_model_compile_definitions(NAME "gap.udma.udma_v4_gap9_v2_impl" - -DHAS_I2C - -DHAS_HYPER - -DHAS_MRAM -+ -DHAS_EMPTY_SFU - ) - - vp_model_include_directories(NAME "gap.udma.udma_v4_gap9_v2_impl" -diff --git a/gvsoc/gvsoc_gap/gap/udma/sfu/udma_sfu_v1_empty.cpp b/gvsoc/gvsoc_gap/gap/udma/sfu/udma_sfu_v1_empty.cpp -index 821fd25f8..52dd506e1 100644 ---- a/gvsoc/gvsoc_gap/gap/udma/sfu/udma_sfu_v1_empty.cpp -+++ b/gvsoc/gvsoc_gap/gap/udma/sfu/udma_sfu_v1_empty.cpp -@@ -25,11 +25,101 @@ - - - --Sfu_periph_empty::Sfu_periph_empty(udma *top, int id, int itf_id) : Udma_periph(top, id) -+Sfu_periph_empty::Sfu_periph_empty(udma *top, int id, int itf_id) : Udma_periph(top, "sfu" + std::to_string(itf_id), id) - { - std::string itf_name = "sfu" + std::to_string(itf_id); - top->traces.new_trace(itf_name, &this->trace, vp::DEBUG); - top->new_master_port("sfu_irq", &this->irq); -+ top->new_master_port("sfu_pdm_out_0", &this->irq); -+ top->new_master_port("sfu_pdm_out_1", &this->irq); -+ top->new_master_port("sfu_pdm_out_2", &this->irq); -+ top->new_master_port("sfu_pdm_out_3", &this->irq); -+ top->new_master_port("sfu_pdm_out_4", &this->irq); -+ top->new_master_port("sfu_pdm_out_5", &this->irq); -+ -+ top->new_master_port("sfu_pdm_in_0", &this->irq); -+ top->new_master_port("sfu_pdm_in_1", &this->irq); -+ top->new_master_port("sfu_pdm_in_2", &this->irq); -+ top->new_master_port("sfu_pdm_in_3", &this->irq); -+ top->new_master_port("sfu_pdm_in_4", &this->irq); -+ top->new_master_port("sfu_pdm_in_5", &this->irq); -+ top->new_master_port("sfu_pdm_in_6", &this->irq); -+ top->new_master_port("sfu_pdm_in_7", &this->irq); -+ top->new_master_port("sfu_pdm_in_8", &this->irq); -+ top->new_master_port("sfu_pdm_in_9", &this->irq); -+ top->new_master_port("sfu_pdm_in_10", &this->irq); -+ top->new_master_port("sfu_pdm_in_11", &this->irq); -+ -+ top->new_master_port("sfu_ws_in_0", &this->irq); -+ top->new_master_port("sfu_ws_in_1", &this->irq); -+ top->new_master_port("sfu_ws_in_2", &this->irq); -+ -+ -+ // top->new_slave_port("sfu_stream_in_ready_0", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_1", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_2", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_3", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_4", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_5", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_6", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_7", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_8", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_9", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_10", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_11", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_12", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_13", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_14", &this->in); -+ // top->new_slave_port("sfu_stream_in_ready_15", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_0", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_1", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_2", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_3", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_4", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_5", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_6", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_7", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_8", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_9", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_10", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_11", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_12", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_13", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_14", &this->in); -+ // top->new_slave_port("sfu_stream_out_data_15", &this->in); + # There must a newline here since the build version is appended in the release process + export GAP_SDK_VERSION=release-v5.21.1 + -+ top->new_master_port("sfu_stream_in_data_0", &this->irq); -+ top->new_master_port("sfu_stream_in_data_1", &this->irq); -+ top->new_master_port("sfu_stream_in_data_2", &this->irq); -+ top->new_master_port("sfu_stream_in_data_3", &this->irq); -+ top->new_master_port("sfu_stream_in_data_4", &this->irq); -+ top->new_master_port("sfu_stream_in_data_5", &this->irq); -+ top->new_master_port("sfu_stream_in_data_6", &this->irq); -+ top->new_master_port("sfu_stream_in_data_7", &this->irq); -+ top->new_master_port("sfu_stream_in_data_8", &this->irq); -+ top->new_master_port("sfu_stream_in_data_9", &this->irq); -+ top->new_master_port("sfu_stream_in_data_10", &this->irq); -+ top->new_master_port("sfu_stream_in_data_11", &this->irq); -+ top->new_master_port("sfu_stream_in_data_12", &this->irq); -+ top->new_master_port("sfu_stream_in_data_13", &this->irq); -+ top->new_master_port("sfu_stream_in_data_14", &this->irq); -+ top->new_master_port("sfu_stream_in_data_15", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_0", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_1", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_2", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_3", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_4", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_5", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_6", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_7", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_8", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_9", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_10", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_11", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_12", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_13", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_14", &this->irq); -+ top->new_master_port("sfu_stream_out_ready_15", &this->irq); - } - - ++export WITH_EMPTY_SFU=1 diff --git a/install_python_deps.sh b/install_python_deps.sh -index 4b0dbb728..8d5610678 100755 +index cac3e1885..578bb5f71 100755 --- a/install_python_deps.sh +++ b/install_python_deps.sh -@@ -60,7 +60,7 @@ then - pip3 install -r tools/nntool/tests/requirements.txt +@@ -69,7 +69,7 @@ then + python -m pip install -r tools/nntool/tests/requirements.txt fi -- pip3 install -r tools/audio-framework/requirements.txt -+# pip3 install -r tools/audio-framework/requirements.txt - pip3 install -r utils/gapy_v2/requirements.txt - pip3 install -r doc/requirements.txt - fi +- python -m pip install -r tools/audio-framework/requirements.txt ++ # python -m pip install -r tools/audio-framework/requirements.txt + python -m pip install -r utils/gapy_v2/requirements.txt + python -m pip install -r doc/requirements.txt + diff --git a/utils/cmake/at.cmake b/utils/cmake/at.cmake -index 476b87c78..ee6449a1d 100644 +index e03d6b6d5..b7f0104d7 100644 --- a/utils/cmake/at.cmake +++ b/utils/cmake/at.cmake -@@ -909,7 +909,7 @@ macro(_at_model_setup_int) +@@ -919,7 +919,7 @@ macro(_at_model_setup_int) OUTPUT ${PARAM_MODEL_EXE_PATH} DEPENDS ${PARAM_MODEL_PATH} ${_MODEL_GEN_SRCS} ${TILER_LIB} COMMAND ${CMAKE_COMMAND} -E make_directory ${_MODEL_EXE_DIR} @@ -272,78 +58,24 @@ index 476b87c78..ee6449a1d 100644 # global compile model target diff --git a/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py b/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py -index a7b191322..b0c5c5e77 100644 +index 62ee5cac2..d872073f7 100644 --- a/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py +++ b/utils/gapy_v2/bin/gapylib/chips/gap/gap9_v2/board_runner.py -@@ -159,7 +159,7 @@ class Openocd(): - if self.args.openocd_tools is None: - raise RuntimeError("Argument --openocd-tools is missing") - -- def connect(self, use_existing_port=6666): -+ def connect(self, telnet_port=None, telnet_host='localhost'): - """Connect to target. - - This will launch OpenOCD with the telnet proxy and connect to it so that this class -@@ -169,10 +169,10 @@ class Openocd(): - # To allow the execution of several gapy in parallel, we cannot use a fixed port. - # Iterate until we find an available one. - retry_nb = 0 -- if use_existing_port is not None: -+ if telnet_port is not None: - success = True -- port = use_existing_port -- self.use_existing_port = use_existing_port -+ port = telnet_port -+ self.telnet_port = telnet_port - else: - success = False - while retry_nb < 30: -@@ -208,7 +208,7 @@ class Openocd(): - - # Now that OpenOCD was successfully launched with proxy openocd, launch a telnet. - if success is True: -- self.telnet = pexpect.spawn(f'telnet localhost {port}', encoding='utf-8', echo=False) -+ self.telnet = pexpect.spawn(f'telnet {telnet_host} {port}', encoding='utf-8', echo=False) - match = self.telnet.expect(['Open On-Chip Debugger'], timeout=None) +@@ -342,7 +342,7 @@ class Openocd(): else: raise RuntimeError('Failed to connect to openocd after 30 retries') -@@ -472,6 +472,12 @@ class Runner(): - parser.add_argument("--gdb-port", dest="gdb_port", default=3333, type=int, - help="GDB port") -+ parser.add_argument("--telnet-port", dest="telnet_port", default=None, type=int, -+ help="Telnet port") -+ -+ parser.add_argument("--telnet-host", dest="telnet_host", default='localhost', type=str, -+ help="Telnet host") -+ - parser.add_argument("--wsl", dest = "wsl", type=str, default=None, - help = "Launch command in wsl environment") - -@@ -602,7 +608,7 @@ class Runner(): - - if ocd is None: - ocd = Openocd(args) -- ocd.connect() -+ ocd.connect(args.telnet_port, args.telnet_host) - - self.__flash_image(args, ocd, flash, base_addr, first_index, last) +- self.telnet = pexpect.spawn(f'telnet localhost {port}', encoding='utf-8', echo=False) ++ self.telnet = pexpect.spawn(f'telnet host.docker.internal {port}', encoding='utf-8', echo=False) + match = self.telnet.expect(['Open On-Chip Debugger'], timeout=None) + def write(self, addr: int, value: int): diff --git a/utils/openocd_tools/tcl/gap9revb.tcl b/utils/openocd_tools/tcl/gap9revb.tcl -index 4a66dfa27..dea57196b 100644 +index 9e67f67c7..697483112 100644 --- a/utils/openocd_tools/tcl/gap9revb.tcl +++ b/utils/openocd_tools/tcl/gap9revb.tcl -@@ -3,7 +3,7 @@ adapter_khz 5000 - - config_reset 0x1 - --target create $_FC riscv -chain-position $_TAP_RISCV -coreid 0x9 -+target create $_FC riscv -chain-position $_TAP_RISCV -coreid 0x9 - - gdb_report_data_abort enable - gdb_report_register_access_error enable -@@ -23,8 +23,8 @@ proc jtag_init {} { - gap_reset 0 100 +@@ -24,8 +24,8 @@ proc jtag_init {} { + #gap_reset 0 100 # wait for jtag ready - poll_confreg 0x1 @@ -351,9 +83,9 @@ index 4a66dfa27..dea57196b 100644 + # poll_confreg 0x1 + # echo "INIT: confreg polling done" - $::_FC arp_examine - echo "examine done" -@@ -36,8 +36,8 @@ proc init_reset {mode} { + #UNCOMMENT IF YOU UNCOMMENT gap_init 1 + #gap_start 1 +@@ -39,8 +39,8 @@ proc init_reset {mode} { #targets $::_FC gap_reset 1 100 # wait for jtag ready diff --git a/Makefile b/Makefile index 075a716c62..e2d7510348 100644 --- a/Makefile +++ b/Makefile @@ -55,9 +55,12 @@ CHIMERA_SDK_COMMIT_HASH ?= b2392f6efcff75c03f4c65eaf3e12104442b22ea XTL_VERSION ?= 0.7.5 XSIMD_VERSION ?= 13.2.0 XTENSOR_VERSION ?= 0.25.0 -GAP9_SDK_COMMIT_HASH ?= 897955d7ab326bd31684429eb16a2e485ab89afb # dev-v5.19.2 +# GAP9_SDK_COMMIT_HASH ?= 897955d7ab326bd31684429eb16a2e485ab89afb # dev-v5.19.2 # GAP9_SDK_COMMIT_HASH ?= dfabdddd0e78b9b750a0eb46eee85d5a2c9ae853 # dev-v5.20.4 -GAP_SDK_URL ?= 'git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git' +# GAP_SDK_URL ?= 'git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git' +# GAP9_SDK_COMMIT_HASH ?= 1796873cec9ca1feb352a6fe980b627df979bdd1 # v5.21.1 +GAP9_SDK_COMMIT_HASH ?= 8c42b65338e554ac73c749f94ecddd23a9ee5490 # v5.21.1-staging-1 +GAP_SDK_URL ?= 'git@github.com:pulp-platform/gap-sdk.git' OS := $(shell uname -s) ARCH:= $(shell uname -m) From 646563dcaa3b41ff4f8157f185f113c0aac06a7b Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Thu, 12 Feb 2026 21:15:55 +0100 Subject: [PATCH 11/79] Print memory usage by default --- DeeployTest/Platforms/GAP9/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DeeployTest/Platforms/GAP9/CMakeLists.txt b/DeeployTest/Platforms/GAP9/CMakeLists.txt index 0a7fde9c00..db06a4e38f 100644 --- a/DeeployTest/Platforms/GAP9/CMakeLists.txt +++ b/DeeployTest/Platforms/GAP9/CMakeLists.txt @@ -29,4 +29,7 @@ target_compile_options(network PRIVATE -Wno-error ) +target_link_options(${ProjectId} PRIVATE + -Wl,--print-memory-usage +) link_compile_dump(${TESTNAME}) From b2b43a5c67dc571ca3fd7b9127f6a1d5af10fcfd Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Fri, 13 Feb 2026 11:30:54 +0100 Subject: [PATCH 12/79] Cleanup Makefile --- Makefile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e2d7510348..5f381502bf 100644 --- a/Makefile +++ b/Makefile @@ -55,9 +55,7 @@ CHIMERA_SDK_COMMIT_HASH ?= b2392f6efcff75c03f4c65eaf3e12104442b22ea XTL_VERSION ?= 0.7.5 XSIMD_VERSION ?= 13.2.0 XTENSOR_VERSION ?= 0.25.0 -# GAP9_SDK_COMMIT_HASH ?= 897955d7ab326bd31684429eb16a2e485ab89afb # dev-v5.19.2 -# GAP9_SDK_COMMIT_HASH ?= dfabdddd0e78b9b750a0eb46eee85d5a2c9ae853 # dev-v5.20.4 -# GAP_SDK_URL ?= 'git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git' + # GAP9_SDK_COMMIT_HASH ?= 1796873cec9ca1feb352a6fe980b627df979bdd1 # v5.21.1 GAP9_SDK_COMMIT_HASH ?= 8c42b65338e554ac73c749f94ecddd23a9ee5490 # v5.21.1-staging-1 GAP_SDK_URL ?= 'git@github.com:pulp-platform/gap-sdk.git' From 1a075d0242c9986fab53cfe63d59a03f8da0ae5c Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Fri, 13 Feb 2026 16:23:24 +0100 Subject: [PATCH 13/79] Try to fix private GAP9 SDK access issue --- .github/workflows/docker-build-deeploy-gap9.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker-build-deeploy-gap9.yml b/.github/workflows/docker-build-deeploy-gap9.yml index 76e7c6b2fa..e3dac03274 100644 --- a/.github/workflows/docker-build-deeploy-gap9.yml +++ b/.github/workflows/docker-build-deeploy-gap9.yml @@ -96,6 +96,11 @@ jobs: env: OWNER: "${{ github.repository_owner }}" + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Build and push final Deeploy image id: build uses: docker/build-push-action@v6 @@ -108,6 +113,7 @@ jobs: push: true build-args: | DEEPLOY_IMAGE=${{ github.event.inputs.docker_image_deeploy }} + ssh: default outputs: type=image,name=ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9,annotation-index=true,name-canonical=true,push=true - name: Extract image digest @@ -142,14 +148,3 @@ jobs: ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9:latest, ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9:${{ needs.prepare.outputs.docker_tag }} push: true - - - name: Set package visibility to internal - uses: actions/github-script@v7 - with: - script: | - await github.rest.packages.updatePackageVersionVisibility({ - package_type: 'container', - package_name: 'deeploy-gap9', - visibility: 'internal', - org: context.repo.owner - }); From 2bb1bf576f2fbad71d4275d19e1b46e5404072fd Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Mon, 16 Feb 2026 17:05:33 +0100 Subject: [PATCH 14/79] Use pre-build GAP9 GCC --- .github/workflows/_runner-gap9-tiled.yml | 1 - .github/workflows/_runner-gap9.yml | 1 - .../workflows/infra-generate-ccache-gap9.yml | 1 - Container/Dockerfile.deeploy-gap9 | 27 ----------- Container/gap9-amd64.list | 8 ---- Container/gap9-sources.list | 46 ------------------- Makefile | 35 ++++++-------- README_GAP9.md | 3 +- 8 files changed, 17 insertions(+), 105 deletions(-) delete mode 100644 Container/gap9-amd64.list delete mode 100644 Container/gap9-sources.list diff --git a/.github/workflows/_runner-gap9-tiled.yml b/.github/workflows/_runner-gap9-tiled.yml index d456f9f353..a5c8b3ac98 100644 --- a/.github/workflows/_runner-gap9-tiled.yml +++ b/.github/workflows/_runner-gap9-tiled.yml @@ -45,7 +45,6 @@ jobs: source /app/install/gap9-sdk/.gap9-venv/bin/activate source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation - export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 cd DeeployTest mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache diff --git a/.github/workflows/_runner-gap9.yml b/.github/workflows/_runner-gap9.yml index d5d8d8e4c0..e1d6e452a6 100644 --- a/.github/workflows/_runner-gap9.yml +++ b/.github/workflows/_runner-gap9.yml @@ -45,7 +45,6 @@ jobs: source /app/install/gap9-sdk/.gap9-venv/bin/activate source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation - export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 cd DeeployTest mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache diff --git a/.github/workflows/infra-generate-ccache-gap9.yml b/.github/workflows/infra-generate-ccache-gap9.yml index b54f43ce4c..038789ce40 100644 --- a/.github/workflows/infra-generate-ccache-gap9.yml +++ b/.github/workflows/infra-generate-ccache-gap9.yml @@ -40,7 +40,6 @@ jobs: source /app/install/gap9-sdk/.gap9-venv/bin/activate source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation - export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 cd DeeployTest mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 index 05d3993461..c8320e0e41 100644 --- a/Container/Dockerfile.deeploy-gap9 +++ b/Container/Dockerfile.deeploy-gap9 @@ -73,33 +73,6 @@ RUN apt-get update && \ flex && \ rm -rf /var/lib/apt/lists/* - -COPY Container/gap9-amd64.list Container/gap9-sources.list ./ -# Install AMD64 libraries on ARM64 for GCC GAP9 compiler support -RUN < Date: Mon, 16 Feb 2026 17:29:43 +0100 Subject: [PATCH 15/79] Fix Typos --- Container/Dockerfile.deeploy-gap9 | 4 ++-- Container/Makefile | 22 +++++++++++----------- README_GAP9.md | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 index c8320e0e41..4375b3e2aa 100644 --- a/Container/Dockerfile.deeploy-gap9 +++ b/Container/Dockerfile.deeploy-gap9 @@ -2,8 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 -ARG DEEPOY_IMAGE=ghcr.io/pulp-platform/deeploy:latest -FROM ${DEEPOY_IMAGE} AS deeploy +ARG DEEPLOY_IMAGE=ghcr.io/pulp-platform/deeploy:latest +FROM ${DEEPLOY_IMAGE} AS deeploy ARG TARGETPLATFORM ARG DEBIAN_FRONTEND=noninteractive diff --git a/Container/Makefile b/Container/Makefile index e38d99c519..78f1bf2532 100644 --- a/Container/Makefile +++ b/Container/Makefile @@ -4,8 +4,8 @@ # Variables TOOLCHAIN_IMAGE ?= ghcr.io/pulp-platform/deeploy-toolchain:latest -DEEPOY_IMAGE ?= ghcr.io/pulp-platform/deeploy:latest -DEEPOY_GAP9_IMAGE ?= ghcr.io/pulp-platform/deeploy-gap9:latest +DEEPLOY_IMAGE ?= ghcr.io/pulp-platform/deeploy:latest +DEEPLOY_GAP9_IMAGE ?= ghcr.io/pulp-platform/deeploy-gap9:latest SSH_PRIVATE_KEY ?= ~/.ssh/id_ed25519 @@ -23,15 +23,15 @@ help: @echo "" @echo "Build Variables:" @echo " TOOLCHAIN_IMAGE # Name of the toolchain image (default: $(TOOLCHAIN_IMAGE))" - @echo " DEEPOY_IMAGE # Name of the deeploy image (default: $(DEEPOY_IMAGE))" - @echo " DEEPOY_GAP9_IMAGE # Name of the GAP9 deeploy image (default: $(DEEPOY_GAP9_IMAGE))" + @echo " DEEPLOY_IMAGE # Name of the deeploy image (default: $(DEEPLOY_IMAGE))" + @echo " DEEPLOY_GAP9_IMAGE # Name of the GAP9 deeploy image (default: $(DEEPLOY_GAP9_IMAGE))" @echo " SSH_PRIVATE_KEY # Path to SSH private key for GAP9 build (default: $(SSH_PRIVATE_KEY))" @echo "" @echo "Example Usage:" @echo " make toolchain TOOLCHAIN_IMAGE=my-toolchain:latest" - @echo " make deeploy DEEPOY_IMAGE=my-deeploy:latest" - @echo " make deeploy-gap9 DEEPOY_GAP9_IMAGE=my-deeploy-gap9:latest" - @echo " make all TOOLCHAIN_IMAGE=my-toolchain:latest DEEPOY_IMAGE=my-deeploy:latest" + @echo " make deeploy DEEPLOY_IMAGE=my-deeploy:latest" + @echo " make deeploy-gap9 DEEPLOY_GAP9_IMAGE=my-deeploy-gap9:latest" + @echo " make all TOOLCHAIN_IMAGE=my-toolchain:latest DEEPLOY_IMAGE=my-deeploy:latest" @echo " make deeploy-gap9 SSH_PRIVATE_KEY=/path/to/key" # Build only the toolchain image @@ -46,7 +46,7 @@ deeploy: --ssh default \ -f Dockerfile.deeploy \ --build-arg BASE_IMAGE=$(TOOLCHAIN_IMAGE) \ - -t $(DEEPOY_IMAGE) .. + -t $(DEEPLOY_IMAGE) .. # Build the GAP9 Deeploy image deeploy-gap9: @@ -55,12 +55,12 @@ deeploy-gap9: DOCKER_BUILDKIT=1 docker build \ --ssh default \ -f Dockerfile.deeploy-gap9 \ - --build-arg DEEPOY_IMAGE=$(DEEPOY_IMAGE) \ - -t $(DEEPOY_GAP9_IMAGE) .. + --build-arg DEEPLOY_IMAGE=$(DEEPLOY_IMAGE) \ + -t $(DEEPLOY_GAP9_IMAGE) .. # Build all images all: toolchain deeploy deeploy-gap9 # Clean all images (optional and dangerous) clean: - docker rmi $(TOOLCHAIN_IMAGE) $(DEEPOY_IMAGE) $(DEEPOY_GAP9_IMAGE) || true + docker rmi $(TOOLCHAIN_IMAGE) $(DEELPOY_IMAGE) $(DEELPOY_GAP9_IMAGE) || true diff --git a/README_GAP9.md b/README_GAP9.md index 68773aa845..8b561574aa 100644 --- a/README_GAP9.md +++ b/README_GAP9.md @@ -14,10 +14,10 @@ To build a local version of the Deeploy Docker image with GAP9 support using the cd Container # Build the Deeploy image with the upstream toolchain image -make deeploy-gap9 DEEPOY_GAP9_IMAGE=deeploy-gap9:latest +make deeploy-gap9 DEEPLOY_GAP9_IMAGE=deeploy-gap9:latest # If you want to specify a custom SSH key path, use: -make deeploy-gap9 DEEPOY_GAP9_IMAGE=deeploy-gap9:latest SSH_PRIVATE_KEY=/path/to/your/private/key +make deeploy-gap9 DEEPLOY_GAP9_IMAGE=deeploy-gap9:latest SSH_PRIVATE_KEY=/path/to/your/private/key ``` Or, to build the toolchain, Deeploy and GAP9 images locally, use: @@ -25,10 +25,10 @@ Or, to build the toolchain, Deeploy and GAP9 images locally, use: cd Container # To build the Deeploy container with the local toolchain image -make deeploy TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPOY_IMAGE=deeploy:gap9 +make deeploy TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPLOY_IMAGE=deeploy:gap9 # To build the Deeploy GAP9 container with the local toolchain image -make deeploy-gap9 TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPOY_IMAGE=deeploy:gap9 DEEPOY_GAP9_IMAGE=deeploy-gap9:latest +make deeploy-gap9 TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPLOY_IMAGE=deeploy:gap9 DEEPLOY_GAP9_IMAGE=deeploy-gap9:latest ``` ### Use The Docker Container From c6bc2c6458111f08b24283ff2240d52ce767a6bf Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Mon, 16 Feb 2026 17:44:10 +0100 Subject: [PATCH 16/79] Partially revert a16c1c757928ed37f89d2744609fd97bb3ddd702 We still need x86 compilation for Autotiler --- Container/Dockerfile.deeploy-gap9 | 27 ++++++++++++++++++ Container/gap9-amd64.list | 8 ++++++ Container/gap9-sources.list | 46 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 Container/gap9-amd64.list create mode 100644 Container/gap9-sources.list diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 index 4375b3e2aa..6d6f7a2db3 100644 --- a/Container/Dockerfile.deeploy-gap9 +++ b/Container/Dockerfile.deeploy-gap9 @@ -73,6 +73,33 @@ RUN apt-get update && \ flex && \ rm -rf /var/lib/apt/lists/* + +COPY Container/gap9-amd64.list Container/gap9-sources.list ./ +# Install AMD64 libraries on ARM64 for GCC GAP9 compiler support +RUN < Date: Mon, 16 Feb 2026 18:17:39 +0100 Subject: [PATCH 17/79] Build AutoTiler --- Container/Dockerfile.deeploy-gap9 | 32 +-------------------- Container/gap9-amd64.list | 8 ------ Container/gap9-arm64.patch | 13 --------- Container/gap9-sources.list | 46 ------------------------------- scripts/gap9-build_sdk.sh | 3 +- 5 files changed, 3 insertions(+), 99 deletions(-) delete mode 100644 Container/gap9-amd64.list delete mode 100644 Container/gap9-sources.list diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 index 6d6f7a2db3..734674945a 100644 --- a/Container/Dockerfile.deeploy-gap9 +++ b/Container/Dockerfile.deeploy-gap9 @@ -73,33 +73,6 @@ RUN apt-get update && \ flex && \ rm -rf /var/lib/apt/lists/* - -COPY Container/gap9-amd64.list Container/gap9-sources.list ./ -# Install AMD64 libraries on ARM64 for GCC GAP9 compiler support -RUN < Date: Mon, 16 Feb 2026 19:32:21 +0100 Subject: [PATCH 18/79] CodeAIRabbit Feedback --- Container/Makefile | 2 +- README_GAP9.md | 22 +++++++++++++++++++++- scripts/gap9-build_sdk.sh | 2 +- scripts/gap9-run.sh | 2 -- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Container/Makefile b/Container/Makefile index 78f1bf2532..e2ba85a65d 100644 --- a/Container/Makefile +++ b/Container/Makefile @@ -63,4 +63,4 @@ all: toolchain deeploy deeploy-gap9 # Clean all images (optional and dangerous) clean: - docker rmi $(TOOLCHAIN_IMAGE) $(DEELPOY_IMAGE) $(DEELPOY_GAP9_IMAGE) || true + docker rmi $(TOOLCHAIN_IMAGE) $(DEEPLOY_IMAGE) $(DEEPLOY_GAP9_IMAGE) || true diff --git a/README_GAP9.md b/README_GAP9.md index 8b561574aa..08aab49c51 100644 --- a/README_GAP9.md +++ b/README_GAP9.md @@ -3,7 +3,7 @@ > ⚠️ **IMPORTANT NOTE** > This is a work in progress. The GAP9 support in Deeploy is experimental and may not be fully functional. -To use Deeploy with GAP9, a custom Docker container is required because the official Deeploy Docker image does yet not include the necessary SDKs and dependencies for GAP9 development, because they are not publicly available. +To use Deeploy with GAP9, a custom Docker container is required because the official Deeploy Docker image does not yet include the necessary SDKs and dependencies for GAP9 development, because they are not publicly available. ### Build The Docker Container @@ -94,6 +94,26 @@ Terminal 2 (containers + attach device): - Set USB device IDs: `USBIP_VENDOR=15ba USBIP_PRODUCT=002b` - Change USB/IP host: `USBIP_HOST=host.docker.internal` +#### Linux note: host.docker.internal workaround +The GAP9 workflow spawns telnet using `host.docker.internal`. This hostname is provided by Docker Desktop only (macOS/Windows). On Linux, you must either add the host-gateway alias when starting Docker or override the host in the script, otherwise telnet connections will fail. + +Option 1 (Docker 20.10+): start Docker with the host-gateway alias so `host.docker.internal` resolves: +```sh +dockerd --add-host=host.docker.internal:host-gateway +``` + +Option 2 (recommended for Linux): run the script with a USB/IP host override: +```sh +USBIP_HOST=localhost ./scripts/gap9-run.sh start +``` + +Or use the script flag to set the host explicitly: +```sh +./scripts/gap9-run.sh -h localhost start +``` + +If you are on Linux and not using Docker Desktop, prefer the USBIP_HOST override (or `-h`) unless you already manage `dockerd` with the host-gateway alias. + To stop everything: ```sh ./scripts/gap9-run.sh stop diff --git a/scripts/gap9-build_sdk.sh b/scripts/gap9-build_sdk.sh index 819f1680a2..3d044b9b90 100755 --- a/scripts/gap9-build_sdk.sh +++ b/scripts/gap9-build_sdk.sh @@ -14,7 +14,7 @@ ROOT_DIR="${ROOT_DIR:-$(cd "$(dirname "$0")" && pwd)}" GAP9_SDK_INSTALL_DIR="${GAP9_SDK_INSTALL_DIR:?GAP9_SDK_INSTALL_DIR must be set}" GAP9_SDK_COMMIT_HASH="${GAP9_SDK_COMMIT_HASH:-897955d7ab326bd31684429eb16a2e485ab89afb}" -GAP_SDK_URL="${GAP_SDK_URL:-'git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git'}" +GAP_SDK_URL="${GAP_SDK_URL:-git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git}" echo "Preparing GAP9 SDK in: ${GAP9_SDK_INSTALL_DIR}" diff --git a/scripts/gap9-run.sh b/scripts/gap9-run.sh index 912f062819..8c38c551f9 100755 --- a/scripts/gap9-run.sh +++ b/scripts/gap9-run.sh @@ -224,7 +224,6 @@ cmd_setup_usbip_host() { python3 -m venv "$PYUSBIP_DIR/.venv" log_info "Installing pyusbip dependencies..." - # shellcheck disable=SC1091 . "$PYUSBIP_DIR/.venv/bin/activate" pip install --upgrade pip pip install libusb1 @@ -242,7 +241,6 @@ cmd_start_usbip_host() { log_info "This process will run in the foreground. Press Ctrl+C to stop." cd "$PYUSBIP_DIR" && - # shellcheck disable=SC1091 . .venv/bin/activate && python pyusbip.py } From 6d1c8c305b95db714ed112a2695d9c598dbc641b Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Tue, 17 Feb 2026 11:02:39 +0100 Subject: [PATCH 19/79] Update Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4e08ba71..a277d2361d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This file contains the changelog for the Deeploy project. The changelog is divid ### List of Pull Requests +- Add GAP9 Container Support [#163](https://github.com/pulp-platform/Deeploy/pull/163) - Extend Codeowners [#164](https://github.com/pulp-platform/Deeploy/pull/164) - Support for MaxPool1D and RQSConv1D for PULPOpen [#146](https://github.com/pulp-platform/Deeploy/pull/146) - Use Pre-Commit in CI [#159](https://github.com/pulp-platform/Deeploy/pull/159) @@ -12,10 +13,14 @@ This file contains the changelog for the Deeploy project. The changelog is divid - Update CLI interface Across Project, Fix Tutorial, and Remove Legacy Test [#157](https://github.com/pulp-platform/Deeploy/pull/157) ### Added +- GAP9 Container Support with ARM64 architecture support +- `zsh` and `oh-my-zsh` plugin installation in containers +- Shell Format pre-commit hook - Add integer MaxPool1D for Generic platform and RQSConv1D support for PULPOpen, with corresponding kernel tests. - Added GAP9 Platform Support: Deployer, Bindings, Templates, Tiler, DMA (L3Dma/MchanDma), target library, CI workflows ### Changed +- Cleaned up Docker flow to use a temporary build folder - Switch CI to use pre-commit for linting - Update `pulp-nnx` and `pulp-nn-mixed` submodules to their latest versions - PULP-NN moved to TargetLibraries third-party folder From 6db1c52b376d0473621afba8b2dc2ed898d90e5e Mon Sep 17 00:00:00 2001 From: Philip Wiese Date: Tue, 17 Feb 2026 11:17:56 +0100 Subject: [PATCH 20/79] CodeAIRabbit Feedback --- Container/Dockerfile.deeploy-gap9 | 66 +++++++++++++++---------------- Makefile | 2 +- README_GAP9.md | 34 +--------------- scripts/gap9-build_sdk.sh | 4 +- scripts/gap9-run.sh | 3 +- 5 files changed, 38 insertions(+), 71 deletions(-) diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 index 734674945a..21724019b4 100644 --- a/Container/Dockerfile.deeploy-gap9 +++ b/Container/Dockerfile.deeploy-gap9 @@ -26,51 +26,49 @@ COPY Makefile ./ RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y \ - git-lfs \ + autoconf \ + automake \ + bison \ ccache \ - ninja-build \ - pkg-config \ - ibglib2.0-dev \ - libpixman-1-dev \ - python3 \ - python3-pip \ - python3-dev \ - python-is-python3 \ - python3.10-venv \ - python3.10-distutils \ + clang-format \ curl \ - protobuf-compiler \ + device-tree-compiler \ + doxygen \ + flex \ + g++ \ + gcc \ + gcc-x86-64-linux-gnu \ + gdb-multiarch \ + git-lfs \ + gtkwave \ + ibglib2.0-dev \ libftdi-dev \ libftdi1 \ - doxygen \ + libpixman-1-dev \ libsdl2-dev \ - scons \ - gtkwave \ + libsdl2-ttf-dev \ libsndfile1-dev \ - rsync \ - autoconf \ - automake \ - texinfo \ libtool \ - libsdl2-ttf-dev \ - gcc \ - wget \ - clang-format \ - g++ \ - sudo \ - device-tree-compiler \ - zsh \ + libusb-1.0-0-dev \ nano \ - sudo \ - gdb-multiarch \ + ninja-build \ + pkg-config \ + protobuf-compiler \ + python-is-python3 \ + python3 \ + python3-dev \ + python3-pip \ + python3.10-distutils \ + python3.10-venv \ + rsync \ + scons \ ssh \ - gcc-x86-64-linux-gnu \ + sudo \ telnet \ + texinfo \ usbutils \ - libftdi-dev \ - libusb-1.0-0-dev \ - bison \ - flex && \ + wget \ + zsh && \ rm -rf /var/lib/apt/lists/* RUN --mount=type=cache,target=/ccache \ diff --git a/Makefile b/Makefile index 88512e5966..44a74542ff 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ XTENSOR_VERSION ?= 0.25.0 GAP_RISCV_GCC_COMMIT_HASH ?= fbb9fa450d01c1c170f94af817490f41c5ef7971 # GAP9_SDK_COMMIT_HASH ?= 1796873cec9ca1feb352a6fe980b627df979bdd1 # v5.21.1 GAP9_SDK_COMMIT_HASH ?= 8c42b65338e554ac73c749f94ecddd23a9ee5490 # v5.21.1-staging-1 -GAP_SDK_URL ?= 'git@github.com:pulp-platform/gap-sdk.git' +GAP_SDK_URL ?= git@github.com:pulp-platform/gap-sdk.git OS := $(shell uname -s) ARCH:= $(shell uname -m) diff --git a/README_GAP9.md b/README_GAP9.md index 08aab49c51..d649fbb6e9 100644 --- a/README_GAP9.md +++ b/README_GAP9.md @@ -72,7 +72,7 @@ For board access, use the orchestration script in [scripts/gap9-run.sh](scripts/ #### Start the board workflow (recommended) -This launches a tmux session with two panes: one for the host USB/IP server and one for the GAP9 container. +This launches a tmux session with three panes: one for the host USB/IP server, one for the GAP9 container and one terminal on the host to manage the USB/IP devices. ```sh ./scripts/gap9-run.sh start-tmux @@ -87,34 +87,4 @@ Terminal 1 (host USB/IP server): Terminal 2 (containers + attach device): ```sh ./scripts/gap9-run.sh start -``` - -#### Common options and environment variables -- Use a custom image: `-i your-gap9-image:tag` or `GAP9_IMAGE=your-gap9-image:tag` -- Set USB device IDs: `USBIP_VENDOR=15ba USBIP_PRODUCT=002b` -- Change USB/IP host: `USBIP_HOST=host.docker.internal` - -#### Linux note: host.docker.internal workaround -The GAP9 workflow spawns telnet using `host.docker.internal`. This hostname is provided by Docker Desktop only (macOS/Windows). On Linux, you must either add the host-gateway alias when starting Docker or override the host in the script, otherwise telnet connections will fail. - -Option 1 (Docker 20.10+): start Docker with the host-gateway alias so `host.docker.internal` resolves: -```sh -dockerd --add-host=host.docker.internal:host-gateway -``` - -Option 2 (recommended for Linux): run the script with a USB/IP host override: -```sh -USBIP_HOST=localhost ./scripts/gap9-run.sh start -``` - -Or use the script flag to set the host explicitly: -```sh -./scripts/gap9-run.sh -h localhost start -``` - -If you are on Linux and not using Docker Desktop, prefer the USBIP_HOST override (or `-h`) unless you already manage `dockerd` with the host-gateway alias. - -To stop everything: -```sh -./scripts/gap9-run.sh stop -``` +``` \ No newline at end of file diff --git a/scripts/gap9-build_sdk.sh b/scripts/gap9-build_sdk.sh index 3d044b9b90..21c832927e 100755 --- a/scripts/gap9-build_sdk.sh +++ b/scripts/gap9-build_sdk.sh @@ -20,14 +20,14 @@ echo "Preparing GAP9 SDK in: ${GAP9_SDK_INSTALL_DIR}" if [ -d "${GAP9_SDK_INSTALL_DIR}/.git" ]; then echo "Directory ${GAP9_SDK_INSTALL_DIR} already exists and looks like a git repo. Updating remote URL and fetching latest changes..." - cd "${GAP9_SDK_INSTALL_DIR}" + cd "${GAP9_SDK_INSTALL_DIR}" || exit 1 git remote set-url origin "${GAP_SDK_URL}" || true else echo "Cloning GAP9 SDK..." git clone "${GAP_SDK_URL}" "${GAP9_SDK_INSTALL_DIR}" fi -cd "${GAP9_SDK_INSTALL_DIR}" +cd "${GAP9_SDK_INSTALL_DIR}" || exit 1 echo "Checking out commit ${GAP9_SDK_COMMIT_HASH} (stash and fetch if necessary)" git fetch --all --tags || true git stash || true diff --git a/scripts/gap9-run.sh b/scripts/gap9-run.sh index 8c38c551f9..fa01424df1 100755 --- a/scripts/gap9-run.sh +++ b/scripts/gap9-run.sh @@ -437,7 +437,7 @@ cmd_start_tmux() { local script_path="$0" local opts_escaped="" - for opt in "${opts[@]:-}"; do + for opt in ${opts[@]+"${opts[@]}"}; do if [[ -n "$opt" ]]; then printf -v opt '%q' "$opt" opts_escaped+=" $opt" @@ -495,7 +495,6 @@ cmd_stop_tmux() { # Main Script Logic ######################################################################### -# If no command provided, show help if [[ -z "$command" ]]; then cmd_start_tmux exit 0 From ba6c1e826cc564f09e11a724170c6018c5460b87 Mon Sep 17 00:00:00 2001 From: JanCSEM Date: Thu, 19 Feb 2026 13:45:44 +0000 Subject: [PATCH 21/79] Add single kernel tests for random perturbations --- .../FP32/ZOPerturb/PerturbEggrol/inputs.npz | Bin 0 -> 24832 bytes .../FP32/ZOPerturb/PerturbEggrol/network.onnx | Bin 0 -> 221 bytes .../FP32/ZOPerturb/PerturbEggrol/outputs.npz | Bin 0 -> 24852 bytes .../FP32/ZOPerturb/PerturbNormal/inputs.npz | Bin 0 -> 24832 bytes .../FP32/ZOPerturb/PerturbNormal/network.onnx | Bin 0 -> 217 bytes .../FP32/ZOPerturb/PerturbNormal/outputs.npz | Bin 0 -> 24852 bytes .../FP32/ZOPerturb/PerturbRademacher/inputs.npz | Bin 0 -> 24832 bytes .../ZOPerturb/PerturbRademacher/network.onnx | Bin 0 -> 233 bytes .../ZOPerturb/PerturbRademacher/outputs.npz | Bin 0 -> 24852 bytes .../FP32/ZOPerturb/PerturbTriangle/inputs.npz | Bin 0 -> 24832 bytes .../FP32/ZOPerturb/PerturbTriangle/network.onnx | Bin 0 -> 225 bytes .../FP32/ZOPerturb/PerturbTriangle/outputs.npz | Bin 0 -> 24852 bytes .../FP32/ZOPerturb/PerturbUniform/inputs.npz | Bin 0 -> 24832 bytes .../FP32/ZOPerturb/PerturbUniform/network.onnx | Bin 0 -> 221 bytes .../FP32/ZOPerturb/PerturbUniform/outputs.npz | Bin 0 -> 24852 bytes 15 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbEggrol/inputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbEggrol/network.onnx create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbEggrol/outputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbNormal/inputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbNormal/network.onnx create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbNormal/outputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbRademacher/inputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbRademacher/network.onnx create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbRademacher/outputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbTriangle/inputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbTriangle/network.onnx create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbTriangle/outputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbUniform/inputs.npz create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbUniform/network.onnx create mode 100644 DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbUniform/outputs.npz diff --git a/DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbEggrol/inputs.npz b/DeeployTest/Tests/Kernels/FP32/ZOPerturb/PerturbEggrol/inputs.npz new file mode 100644 index 0000000000000000000000000000000000000000..b58ac20c7bbefbddec5697a7b2b83fa36b8a4cbb GIT binary patch literal 24832 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 z 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Νh=>w**y6G4wW`z$SGa$GW|`@=I4@K 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