Upload 4 files
Browse files- hrm_all_in_one_API_Reference.pdf +225 -0
- hrm_misc.py +1043 -0
- hrm_utils.py +498 -0
- hrm_utils_API_Reference.pdf +200 -0
hrm_all_in_one_API_Reference.pdf
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%PDF-1.4
|
| 2 |
+
%���� ReportLab Generated PDF document http://www.reportlab.com
|
| 3 |
+
1 0 obj
|
| 4 |
+
<<
|
| 5 |
+
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R /F5 6 0 R /F6 7 0 R
|
| 6 |
+
/F7 9 0 R /F8 11 0 R
|
| 7 |
+
>>
|
| 8 |
+
endobj
|
| 9 |
+
2 0 obj
|
| 10 |
+
<<
|
| 11 |
+
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
| 12 |
+
>>
|
| 13 |
+
endobj
|
| 14 |
+
3 0 obj
|
| 15 |
+
<<
|
| 16 |
+
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
| 17 |
+
>>
|
| 18 |
+
endobj
|
| 19 |
+
4 0 obj
|
| 20 |
+
<<
|
| 21 |
+
/BaseFont /Times-Roman /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
| 22 |
+
>>
|
| 23 |
+
endobj
|
| 24 |
+
5 0 obj
|
| 25 |
+
<<
|
| 26 |
+
/BaseFont /ZapfDingbats /Name /F4 /Subtype /Type1 /Type /Font
|
| 27 |
+
>>
|
| 28 |
+
endobj
|
| 29 |
+
6 0 obj
|
| 30 |
+
<<
|
| 31 |
+
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
| 32 |
+
>>
|
| 33 |
+
endobj
|
| 34 |
+
7 0 obj
|
| 35 |
+
<<
|
| 36 |
+
/BaseFont /Times-Bold /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font
|
| 37 |
+
>>
|
| 38 |
+
endobj
|
| 39 |
+
8 0 obj
|
| 40 |
+
<<
|
| 41 |
+
/Contents 20 0 R /MediaBox [ 0 0 612 792 ] /Parent 19 0 R /Resources <<
|
| 42 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 43 |
+
>> /Rotate 0 /Trans <<
|
| 44 |
+
|
| 45 |
+
>>
|
| 46 |
+
/Type /Page
|
| 47 |
+
>>
|
| 48 |
+
endobj
|
| 49 |
+
9 0 obj
|
| 50 |
+
<<
|
| 51 |
+
/BaseFont /Times-Italic /Encoding /WinAnsiEncoding /Name /F7 /Subtype /Type1 /Type /Font
|
| 52 |
+
>>
|
| 53 |
+
endobj
|
| 54 |
+
10 0 obj
|
| 55 |
+
<<
|
| 56 |
+
/Contents 21 0 R /MediaBox [ 0 0 612 792 ] /Parent 19 0 R /Resources <<
|
| 57 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 58 |
+
>> /Rotate 0 /Trans <<
|
| 59 |
+
|
| 60 |
+
>>
|
| 61 |
+
/Type /Page
|
| 62 |
+
>>
|
| 63 |
+
endobj
|
| 64 |
+
11 0 obj
|
| 65 |
+
<<
|
| 66 |
+
/BaseFont /Symbol /Name /F8 /Subtype /Type1 /Type /Font
|
| 67 |
+
>>
|
| 68 |
+
endobj
|
| 69 |
+
12 0 obj
|
| 70 |
+
<<
|
| 71 |
+
/Contents 22 0 R /MediaBox [ 0 0 612 792 ] /Parent 19 0 R /Resources <<
|
| 72 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 73 |
+
>> /Rotate 0 /Trans <<
|
| 74 |
+
|
| 75 |
+
>>
|
| 76 |
+
/Type /Page
|
| 77 |
+
>>
|
| 78 |
+
endobj
|
| 79 |
+
13 0 obj
|
| 80 |
+
<<
|
| 81 |
+
/Contents 23 0 R /MediaBox [ 0 0 612 792 ] /Parent 19 0 R /Resources <<
|
| 82 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 83 |
+
>> /Rotate 0 /Trans <<
|
| 84 |
+
|
| 85 |
+
>>
|
| 86 |
+
/Type /Page
|
| 87 |
+
>>
|
| 88 |
+
endobj
|
| 89 |
+
14 0 obj
|
| 90 |
+
<<
|
| 91 |
+
/Contents 24 0 R /MediaBox [ 0 0 612 792 ] /Parent 19 0 R /Resources <<
|
| 92 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 93 |
+
>> /Rotate 0 /Trans <<
|
| 94 |
+
|
| 95 |
+
>>
|
| 96 |
+
/Type /Page
|
| 97 |
+
>>
|
| 98 |
+
endobj
|
| 99 |
+
15 0 obj
|
| 100 |
+
<<
|
| 101 |
+
/Contents 25 0 R /MediaBox [ 0 0 612 792 ] /Parent 19 0 R /Resources <<
|
| 102 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 103 |
+
>> /Rotate 0 /Trans <<
|
| 104 |
+
|
| 105 |
+
>>
|
| 106 |
+
/Type /Page
|
| 107 |
+
>>
|
| 108 |
+
endobj
|
| 109 |
+
16 0 obj
|
| 110 |
+
<<
|
| 111 |
+
/Contents 26 0 R /MediaBox [ 0 0 612 792 ] /Parent 19 0 R /Resources <<
|
| 112 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 113 |
+
>> /Rotate 0 /Trans <<
|
| 114 |
+
|
| 115 |
+
>>
|
| 116 |
+
/Type /Page
|
| 117 |
+
>>
|
| 118 |
+
endobj
|
| 119 |
+
17 0 obj
|
| 120 |
+
<<
|
| 121 |
+
/PageMode /UseNone /Pages 19 0 R /Type /Catalog
|
| 122 |
+
>>
|
| 123 |
+
endobj
|
| 124 |
+
18 0 obj
|
| 125 |
+
<<
|
| 126 |
+
/Author (Generated by ChatGPT) /CreationDate (D:20251016100229+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20251016100229+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
| 127 |
+
/Subject (\(unspecified\)) /Title (hrm_all_in_one \204 API Reference) /Trapped /False
|
| 128 |
+
>>
|
| 129 |
+
endobj
|
| 130 |
+
19 0 obj
|
| 131 |
+
<<
|
| 132 |
+
/Count 7 /Kids [ 8 0 R 10 0 R 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R ] /Type /Pages
|
| 133 |
+
>>
|
| 134 |
+
endobj
|
| 135 |
+
20 0 obj
|
| 136 |
+
<<
|
| 137 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1742
|
| 138 |
+
>>
|
| 139 |
+
stream
|
| 140 |
+
Gau0C?#SIU'Re<2\-Oe<fj)R\*S^7Q!a1tn"k#<P[&:DFBSj%&:o#ShDR+EbI>=&'a%^A-`f^=Q6A_''glHLe4_j$/F+9sDkgt=pcTUiH$52p@XY'bnF;F4khXda#H_"2uqJ@=XU($@)[pDc%Yglr]_nHPFI.EgS^Lus+gT>XGJ@@g<NG4N/9SbBfTotp8^>GiPbBJ'I'4,VqnH0-tU>SOfH%-c<q&R6-\C9co\E!6d8#8]ro%NfjU9d$@:+0"Cc6C`5i9J0"H?e<Fp+':ZGd(JC42K#Q/=!l@T."<+O\dTPs$]j-h;g&NL2b'?Rp\*N"mHHBl/KB?I!bP#7DUh2'WYo5EI935D)@[F1'3GG-6n^(WJS`a,/LYKRdS[a^8.tX<FP:Nd7j-YQHYS83g%Y1$EfjX\R6:6]YJU4'/gL.:BedSM$IgZhm+dL>dkJ=DMWA3rLIZsH7nT%(Sd%4?Hc@IV8g#H);eDGO!!W@,9%l2hd81S1-Y3q)rK_%B4HM*Yo,^#5i$CIrlo7#L(mC'j+jL:j$dX6*pT7c.2.O%.@Y#u:8]mn_rIVpLX9(VjsEc4P;9"R_AnJcNOdEj"*=,")$@3%rq*u/[cgXM)3T>-<AI,n9mVjhFAbrC`2J7ed9NhCR`+A_ba@@aK,W,8k60Vu)>*1!UOaCrEH[hP_@0#B3=du?ct?7W6G)Dm\X]XdMpTn2^h_PJ.1atQj>.J?Cs:oV`bOLQoUPG2g,U4(RT-)5UCdPA\)t-Ug!$BKk4:D2g;L:\$0<_=6.gHXKQYMH-0#g1#ddjlO\7cdjol!fnmtPM,hHGqHKFZ-hK0"Y>q#@X;VLP)pLmA:JE8d3#Mr2A``A\33O)juro(0@AJ#b#?Xaj8l2f>h)(^qi7'EU1q"8N`)TtE<pgNA5)\49MoJ&"kXtMJ-YI,fCIA16J!5[&fgU!?tTdmo=/adJ`];eTK4ei+*q3k&Tk/"p4%FsW.dAc/q/c+P8i1N_;:^qT,5Q];NQbel.MI4LGPE)bM=ZB[sJ#TPHTX0]/5R:K^dBpTgl13o[Z!C$:b#5@X;,'qg=N'#N(^RXK.N,)I8l[hqN%k5!8?cR?WFWl)h1gO)$$;&%`9&P/d7VbFY)[9S88#nAO#:k_p[Gi)%)=(HcI&XI0U7q=Z,0JbV#/PTPf!(Z(9RQF7d#+<gZV1>/1G85hGS'a)pMC$`cY*=4%"9iYWr=Fhh1%X3D&GNZCiDW_/:;3Kd6\]XYC&g<?k)/ZC`R3c8CTT@dRe]CdYq8_rpEGn#B?bRkS[H]E_4,FTTTjZlZ0l>O.]`;AIg9mgT1SRR><81qSn+nb)l7?cPcgID+M`4E>$&po0#2Nq_pLqtGn`)isf$brtC_+gRu"[n"nON5P+E-3V2?BY&Q=\eH6s_$OEd*=UO$K6n2aTiJ+$LecqRBpKbNVD0Fs45?DER.D5Y"(@/93&dO#G)#4)ZTVu.[q>2K+ocUlnN3mnPiGkG5ZYGm@:g$O?m#4*EM,S>K&sNXYt97GV<'-A.KXgfcn.:gd?IN2T;meqrNi+g.rZ*9r?:""$?<hno[q,`,VW7h.(aub]f:&S_Ss$L<q*8H@Q].E/&a'g$gM!?mq1jlkpTIr+C!-s3q(caHabIZGCqZ?SI3NfaMHAD'>BfjWW\@mC0VbFm,80Q=gHF(UefY&>0P,<+r;d*_P#\;J1=Z+LFT3qV^X5tT(CS8:)%8ECBi1J(Vl*gXb5'&l*I9#4mi1Q;>UQ!~>endstream
|
| 141 |
+
endobj
|
| 142 |
+
21 0 obj
|
| 143 |
+
<<
|
| 144 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1621
|
| 145 |
+
>>
|
| 146 |
+
stream
|
| 147 |
+
Gat=+;,cL5&:WeDm%i*%\E6,JKKqnG1Nsh_4j^fV8Y6:K;/A6n,?m?'-FCJcpTA$g`l>hO^jpEJ=5SJ\:TJVGLu46km6q!s(3"<]J2;dG!7#+/J]/('/WROc3P"6n4IF"=d!XnC7mDHf1_;Dd>#YGOOlU1hmH"*L6PVd%%V)'H,/WQAn?[BKllnC8o+fL.P,+!)b:no7e6hpVf[/8N>#H"+@E!r0E>AA?c:=e]jmb&Vre/60kU@,jS>*X!mY:H95JTK((+6))RBJY-\.S7W6";5%*BN'OTu5!0nqN5)/Ri1l^X]Z]St0NIIV[/F#idHlb]7M5S(bfj+MR.T9bc,q&6DnW[`7QNqL9AG4l8bDD<5>I`u*a$&ukNe%$4ekXqM!q:*U?,cH++nD-4TPn'ZI*R0m5lH('SWaM0e7C6XB-]aLP\@<'QO:%_jf*t\pVgn2EL`!5=Ik^IIKn\^gm<_1!0cW;j)C0!`;k&FQ\bhpufb;u'mM_uD6U[so\d9]p0W>WWgj<BNlQ)_&*eqaX]+Q2Iu&q]/a$^oGCKbmX*P6gf(Kg^[WF$`'K\E62,T4Kp`8uTc9=&7UhR*sQpE8LCCO5]If=JR@0]D&9?$R77I:\N`l<lb`9MLEKm2?EY$WVcRZj=m!\$9;'b0KIDCdBSJHan6L@@9/[_"[fIq>lff\N,<r#]+8$9i7VT?DLMan:b5o`i&4:B+glntl$A>-i0nm)o'09F^^+&L-eE"C"aaC3cb!<7K>F4M)jH/)G($Fs_KS[b-lAk!61q"ESKpUYc@u3Q\nGCPYu=E"IK1mbi]]#B%W4h;BLDPm+:W9m!/[M/`qfahN%bO/A(8.t#otol/*6@RR`Y9jE%meAPOUXa[a6g@4:b<:<Wa]FLfN&^LrqU"`49-m\J%0c:C"<O&o(t!P/M1n0/cSoqhjB>\fu*9.+37:^M!#mAUfffm%H,2Z/8rJLCf<m7BqJ4S'/)r0%^I9k:>P/5RS^]2$A->'0M7`_d">4fH7p+,f/sWY.i].MWudi1&/Ua-ajcp0;%!_;6q+Z<@pl`[#gOdmO#6'2mk-MbL,M^9F5@rG-Qk]V;g5?Ed0Q7F3M+%r,`C%*?D3dI7.f1gVnpQ_AR)'-.#p=[r=>k58\"3SIDQmarH'20jeO(&<hp=o$I3KA2L?hX*KVd$`LG7kbl(to5``C==*$Z0YC8%g6UC`i5A5*pT)!1D,:.-eB^rTiJ+<_2.S(D8t#:Y"o4arrY<MtDMq>rWU,CTRt?4pRC5lCJuTX(2NW]0YjD+$+?p]9Ya-I5*E%tuV_U`-b<FVaoPbg7\TbD(rL?>-U_6Oj^``Ag!B<,FJbc\Y_3oYrIWW'0?/GJ5!$/54B=Fs90msW/V.i.`_qFHGO:Xqp5tE<8-p(%FS`iq[%K58NBt3RSrnTI#2-ITdI;I;U&jm]`9<"3I_q"Ck<J9'2.%ff6V_b:-7Cb#!=liBs;,(Y1-BN91gOm$N<qCu&.Rk_SS"+k,ZAJ4V*pjO7EdXd@s6Ak8I(F5U6OrtGp"&5#'#f4pCW<fnRFfZIehAi3*^"A2`QB5UDjn)6'DB/&B^SPu#+9bWaJ/9:pipM731CsE<_#Ppg]X%fB)10#_9m``jGgk~>endstream
|
| 148 |
+
endobj
|
| 149 |
+
22 0 obj
|
| 150 |
+
<<
|
| 151 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 634
|
| 152 |
+
>>
|
| 153 |
+
stream
|
| 154 |
+
Gat=g?Z2Df'ZJu*'^'$?R8OA;Tj=Qj>16NPKXP^WX]1E!SA\()WU6e@\)2=Y."BgE$ODFT\_"m:HZ+S!mV)QjPe_Ero=G9*N,4BM+7t\l\BVZ605?_C01_05&mC`ghOBYi;l*-!/MBRe1G<#!oZX(0D%*76ia@;<V!J2rF.^qS-7SkURDl[F`H]LRn>48*%^M_JhSKM'6<5Tu(59;N<I-D21+nR7h&T?5q(IHF08_9-WN8pZ/K]YQO8+7O9(H8a4,J'RYqrg'Z(8N:h*gBGS:02G^Ki%.?OuYF@?'a?YINf,BPI,NKak2-/c>(m)N6l9TMe6=CPEZ`"NUSgU+h6t(i!X+XgD83^Jju^Qq=,7C$*&:i9J=iffLh)'U;_67)=fUS&6Z^dEf[KX:-DR=U##E6?5h!:^^[P<e`2]o9ES-TA5dU.17;3Gph7<EQBB[nCYS(X!uOG5JKiL/6ZfX2uQ$(VFo-Kpis=:Arh4M^$R^G+*ei5i8-m7XTb\bGT0nZm&__ZrtnC7a=E5r-QB*:8B,<M\kl+V^[,^RMV8a3DiVBnTBp<r'm"jDfkYDY.%+D/[dB)8NDF`oNSB_ajs;*Dh4KCl]1HjMQhuu:a+NFq7C2-RGgARd5IEq-qZ~>endstream
|
| 155 |
+
endobj
|
| 156 |
+
23 0 obj
|
| 157 |
+
<<
|
| 158 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1425
|
| 159 |
+
>>
|
| 160 |
+
stream
|
| 161 |
+
Gat=*@;k0a%/o$i5o_So*HI/\[qW"_J9C7%)piKYk/0A>Kq[.@U8okkP+5kPlUT(%'.:>t>6DCDo^k[OjJ,$IkSLgC=p"U=_sfR;E!1.jiL,0"nK!"9:]<Naf>qsmO5V:Mcr=2sGi6Tlr.LAj*?2+2M.m+OfJ8EfbN$R!EP]Wl']RFMIqsmjVr&![GVV<T*d-P+F*"u;^Qp),YOG%Zd-uPVTJ2_4AB&$N^_AkDO(&\i'E>^"H4D%`C[OMW<e&2X@;3QbDN/u1$b"j$m&#=4A8S@#,T0[MTT+QliE\78LC7*/WYY&LGYse6<\dtE9AmYu54$O\lq=&^<]'bZA[iGUGU2&X!Ip@9He8[*P2VS%Ql,-r11MY:,b"ac/bu3O@'3VO;4*$5BjK9nZNaTSLV,'t.=9!i@YQl668Ae_pLcYE$k4Eg;ca.Nr6u,P%g/KFq=M$!FI'@KqYn/NT._U[R'P6H(AL2QWu%YU]!=M>fn5:Y6>:Y6#g/$Agj+C1Eh;2%eZ'!C&G,sk/[:?R"Zm9#ZJlUT@klCCYi$fd/MQI`ngYVd_G&+(VJ]P1q^4SN\#*Y8pLc?menlUg/p`]gF^8I>.p6bd(8^B6e=q";<g4K-WpK7W'P\=2^*+q<W(g0a;*=4q_M2]*8?Le:Ua&c@WJ'Sj&r>D)m+b-#WL_2;iWUC>]eGSn&eo0U-CEb(onIEBn4LgKR$otoWV@.C8j'i'akDcRN@$qARXM:H)s)BAaZd6Xc$CO9\&pS@N@8]a/Seo.]K9?3W*lkSWMi?#l!.&!+Ra.#cfo+uPaD"QSqW\K++D>Jq),f*GC3_p8aE@8.1`qcZnoK,84nBN1Xt@T%8p=:j256,=M_*EVRgULep5R_BMEd^9Xfa02XK1P%b#*5!DNUF($ROrE6!`pGtplJQO$=p_2=q/EhE8cH9,9dJK@\56QXku[2(9-7hcQhRhTi?l/i>%HMF;P/4bJ:>uaeUY^P\,hVr*XeoYOF?$!NIfX%oUi5._8*Co:*s/5bN;e(mg\8tpdeSu8]Ir<&1Z\q0[\8QWWo`\W5=BpFhrk6P@!XF.-E/-t.T33>JTgjLohKMfe)$M%cg+(7Gg;pcEDp*4+0CUQ;[r>j<n>`k+0PoVfS8NOX;f?un]>PH&>!]W9!S=D$XK"EIhXK#s,oJO&mHJ<bHUD.[W$Y(h'._$n/e4%OM&#P@\b0GH>,ncF279d0dB%&:pJc-R%lgA/YK5NSNVg%-f53gI>&m?fAt%KUr)-ZD+DinBCiY<>7=EP48cTSL?#DJ^.2H=',#I]ji+,cd.XS#PR2u>3"Ui"ehK/Na?!p5ZQYiHo1_Fl9>"M$EYl7\eA/6sCI\t5=[':>ZfG9YOU]kt5FoYV<%qLuL^sr4T1N`AbNB,]AQPiS);ECT;!n6=);G/?o>t':-IfX$oKS9~>endstream
|
| 162 |
+
endobj
|
| 163 |
+
24 0 obj
|
| 164 |
+
<<
|
| 165 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1357
|
| 166 |
+
>>
|
| 167 |
+
stream
|
| 168 |
+
Gat=*gMZ%0&:Ml+9G],D'Wu8oQS_k?+hNT9$eSR"TY;9T(l>QO:o$]]p81:Y]gGE_ARe5e@DIsQEb<VdbaJ!pLHj\p`u2[j&@rMsquFSJr.-eWE!l?u`SVJ\[<'jl7W?oDiMSZQFZeW$NVHT3&AeiOg2l)l#lL9"8_oCJW$Sf,R8=BO[Lh:h-3^<*b=`-@_VP1XmPZVJi!Q?fm[&\H\.]r-Q"iZc!a23+O>s,J5r23qKo&p+42Ar_ja(nQ6mSG(.?jf6$&_N%AkdY'%.88`_V_$"@:fZp0"#SBocca$Nk_4ENrD!J\)3.q.)JBj,tY&PB^^a#;tk;Nod=5.("FF*pp[YbfBQbF%UeTe&&+p&P9aRG#u!^[C!m%![=hf,2+o@)LbVjG-:O.2rR?XBF?W%e6j<J._7M:X%.!WtaP6tbph?gZ@cUoB,h12n=A4j`LPY>GQWFM0!CCY?0K:!R>n_\RP268V?:C9(]Ru_(-BW:U/Jc9rF_n/(!%!Le=/H1+;e9c^n\Rfh5oZ6Tp9HW9F&7R(8k!]B.Uji)[_s_MQFWXDgM-.p'..ShN6$(78;7q[FAmE3jPrtnAQTL#69g@H)WQb_8!\,jQFlu;jccNFAHh9.XA>d2I6?!f>CRQN';b(./t;Vp4C9CDPVeds<f<\O]Fttl:fGiZk[Vcd-lf)#8uHiY:CQd(63Pl'7J042-Y./P"RUS^8EcM]'Hn?nig!TZ^'3(.9hE0\$^76d7-2fbHZ@^Qm7mrP3"Eh9b-8LA!.=g>d%"?F(U&0&>R/=@i?'DBIj?0J"^Zjb;/j6c;0+1-E8>3,8A\33D0me=K'rj'FuK)&.[PC8CT>GgYF4Cf1K<VNg8,=J)S'cV191l/Yu=]g<#_gQ*ggr9fO2`hVMXCSFt@0eAAXiHbjKl7U'I=blnB%C!dbeNrot]6ohZupW^n';hog6"gkJ.'6eQKkoXb9-$N=;]NMf@*)CDh-Pd/'T#o5@!`<0P;l;Q3)?n4"RN4I3N]i*Jg=_\\-@9-o=]oNk-N.R/Nh.TaJ*G2JUh(UgtMXGtU/EaCV^0fRnFfm.dqTM\caW,ZK;1CQg#,Bo4&?[O=JTD[+9,PQqa(gt3(F;b75?(P5`Q%.lk^Y(eLupI!l*9,s%5R*$U;G>o4M`1um]+$-d1))]0PB@^ZtaiDEfBD$rd9K>p$=KSV[es)FH"V\2!H$IR\,bm^HPlEl5FSAZi4q9\s%HA5=24DXcf>Rm>k,"D5V3TY-6^?<,4F9h;itdW?jV9[r*eDq26OJd,*WZ<JoAP2P4(q3"W^$oA,*sap!fu>>Z](f$aXpD7FmmJ^&-%8hS`sV4?8uQSJF+pNeL)(L6kK.H.S(~>endstream
|
| 169 |
+
endobj
|
| 170 |
+
25 0 obj
|
| 171 |
+
<<
|
| 172 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1292
|
| 173 |
+
>>
|
| 174 |
+
stream
|
| 175 |
+
Gau`RD/\/e&H88.EA0)^*^!Sgh\$#/cnu+`@-ub#rDXL5,FpAD&uh##'<gVLhbgZogU40'-nl$jKd1jrcTQJB%pXFnnf?flJ'=R0kCMg6/-0\I$t/cFgs.PO\a\037t#"QA+u.PU7C:^.9H*K3pP8LkZglOI.0U+POD[<mFNdV%u')]E&!+hgccHNR1@HT_!G-<jrHU3VFHpOBiFua<53O$<1;6OM4nMf)AH@)?%+_7o%$,*10']4Pquo(She7p=sf2_Xu!/\&)kZ[-7*]QbQmNp")X]</n<*P+GB>U`et8;40&N@kle@j]Aae>Mj[uepq&4b$C6+OiK<qH2qm)[5Jo@kQ(j;H*H^5KhfhJ"pj9gfRpbs0RooRG\a*WrHF,n5YSnRFcVpTiBlLL)LW>Wl#_bU)C2*:B;l25qB;K'YUidAJ8?M]9L(<"D/CSRaU'E@b,$D"i1PQbU@u:tfM!N^Q.]2(Y@,E?:?MMh!GYr!6@O2,_4A:mVPm`>1;e4=tLCj'(#H!B79)^UmBX7ml+CRB:."n6FE/=o9iu3'eEDA<*3#nL6iK>Kq,kE3)85ItXN*aZbn_b[FGE!^(mk-]F6S[KY/0Tn&s,60rZ>a`A+LLWm9[36]oaTf^%re!$IYuPUYG"P\]q^gp3YRVnP!t\=+I:D!7mUfT<&ZP)822;,YDB*n<.R^m>b&ZQf3d:O&@?]aCe6Hm`:fkq*%YepptY\>V<@tZ;9B@4-F,YoM,Y4S7)B"j?>ms<$<:2c2kh<D9,ZEHlS_&;C+u^e[HT_:OAMqA]gGb;nG`.3c'qobNuNo3^T&I77CiQ3!k3A4Wo;e[#28O.Jee-oZ>Dteqr=,Jj;b!ZkVW&hDFhIUaZPiY\!MAPfm[,>J*$[EL"=+uTY4=diR2l(60hXtdFY@\qq@KOnt'Ed2H[=5eLE%]9D\$H8UbO05E1DX[3#,G1%r_r]$M)<TXM`7KNadILt9&;dQLU:rb-7GVc64C4-U+O/<"H&(rm=u6Oj>SD3k1+'HT.S[*-Ddf_kEUoB5S7$()DWRXc[gbp8[O$+\].8-cSCg3U2cqsKcN>u30rW>)ut4fD=H3E,;j,uNnnSc!,1[Z5S"!T[BDL1=[/[Fh?Oj'B7Zgd"(BSoO(7KD+8,1o+"T2.G@6]`6H:[kUE]dX)%=-s`$frqON9kBTc<PN.unV"$EMRFG!'[u5XFW^as$dd.&$Ncor?q>0-r/)^8Lkg;9<*i3XY[8q--%G=`KZ4Ejc/W:*J>k4heSW="?.j8+DKCph8=?rAYDSX5@649P&~>endstream
|
| 176 |
+
endobj
|
| 177 |
+
26 0 obj
|
| 178 |
+
<<
|
| 179 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1122
|
| 180 |
+
>>
|
| 181 |
+
stream
|
| 182 |
+
Gau0B?#SIU'Re<2\.DI^)(J(<-,jkZ5-3D@!\)K=HCG.jQ7.[!6ujR7`%C]?G=af<j!>IU1_=&V=5s0<pY8,+&E4A6hm*@Z-&8PY,)B^LCP\,rJc$:4c-QALMmcu)P9>KnpT!T,*]go:T&J(1kA:j-OE]o(!`<X_esl>TnVN]@E=Je=Yb9\M=VKAL78'>KCK=>@*/(>jRR,V.">1ktN/jcjNok>MD;Y:F#]rt:G-I3Zi*PP1ft.4!kFhNb3ZY2JW'#fJi]Ll!F"L?]9YpUD`;Og@\:=0KdG^1\eOQ@TPMr`EnH7>kgh_n*%mnPpmcTqp^BJK(U[X&Te*tl$rn.Qg!+SmF8pEm3_>M/s'kBsUIea1_d^C&0Ug/H_2A__3(uEXGhT68rbiTRo/t[?^lNj]PQ>m9#3Mo[f,hMtRh<k+8jmeGsbufmrVg$H-[6%7Y884fLJ1_S#Ai:9C)ZH\LbDg3=LIQCi!a[Qe#YkK4JY1*dPdg,u;+ho4_;E;cOJZMdrZoshU66oc;XWUV"j+`@EqT[Q`V(p1elERH%SMM5(t&bCio(/43&@&kW$d@<pi@?M2Ui?RRja]#/b3P-TkTb)GjDf2!eB`Td4;#5;pSY$$-l^DY3sr"eVmN4ji`tSVPmU2R,CALL,6,H#)jb)Y'+aI^%(UmJnLFq[V/+?o!O00MPl)`R'IT'K>DB?=+8+&/>\2a0dsRMduu-sjO-/NH#e>=;WSAk@RlZki94(TRLqAV)brGYY:YN5[\@_YS'`2+lBsLG\31oW7$@O'%=aZTqc;FPr-J/#EM!'ZG22'SX,#e6Ds#_Kf3o4;hJ7&>FjXl@>Iug5Bt(L?`4/7$7OEC:e8@ufH[![j`Jt)Xbc%-4H[\4Q9?&!Q^6<he3?L"0)D"jFZI+JpX%GZGrPYg,De*c++buo.Z3@7W#^D+cZ.Q7-aplJpO9OU6hfH6'8u'3=21D2;`g9/BF4-CII&<`kh`lNChjDg)DrH@4R`1pZhU.IAZVs=[PP'a+-K1?PB.)[S.C'lu3sg73=cK9Pdba?olFq*o:l,g'H+[!)E)J+gis$'C"A,Q:aD=Nkr]\$RCfmoJ@Pj+-+%&'nm`D\ebqTD5m\TLe5]HE=;\p>p~>endstream
|
| 183 |
+
endobj
|
| 184 |
+
xref
|
| 185 |
+
0 27
|
| 186 |
+
0000000000 65535 f
|
| 187 |
+
0000000073 00000 n
|
| 188 |
+
0000000178 00000 n
|
| 189 |
+
0000000285 00000 n
|
| 190 |
+
0000000397 00000 n
|
| 191 |
+
0000000506 00000 n
|
| 192 |
+
0000000589 00000 n
|
| 193 |
+
0000000694 00000 n
|
| 194 |
+
0000000802 00000 n
|
| 195 |
+
0000000997 00000 n
|
| 196 |
+
0000001107 00000 n
|
| 197 |
+
0000001303 00000 n
|
| 198 |
+
0000001381 00000 n
|
| 199 |
+
0000001577 00000 n
|
| 200 |
+
0000001773 00000 n
|
| 201 |
+
0000001969 00000 n
|
| 202 |
+
0000002165 00000 n
|
| 203 |
+
0000002361 00000 n
|
| 204 |
+
0000002431 00000 n
|
| 205 |
+
0000002742 00000 n
|
| 206 |
+
0000002844 00000 n
|
| 207 |
+
0000004678 00000 n
|
| 208 |
+
0000006391 00000 n
|
| 209 |
+
0000007116 00000 n
|
| 210 |
+
0000008633 00000 n
|
| 211 |
+
0000010082 00000 n
|
| 212 |
+
0000011466 00000 n
|
| 213 |
+
trailer
|
| 214 |
+
<<
|
| 215 |
+
/ID
|
| 216 |
+
[<94bc6889bc74c708b33a04a081270cf8><94bc6889bc74c708b33a04a081270cf8>]
|
| 217 |
+
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
| 218 |
+
|
| 219 |
+
/Info 18 0 R
|
| 220 |
+
/Root 17 0 R
|
| 221 |
+
/Size 27
|
| 222 |
+
>>
|
| 223 |
+
startxref
|
| 224 |
+
12680
|
| 225 |
+
%%EOF
|
hrm_misc.py
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import os, re, math, json, time, shutil, random
|
| 5 |
+
from dataclasses import dataclass, asdict
|
| 6 |
+
from typing import Optional, Callable, Dict, Any, Iterable, Tuple, List
|
| 7 |
+
from contextlib import nullcontext
|
| 8 |
+
|
| 9 |
+
# Torch & friends
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
from torch import nn
|
| 13 |
+
from torch.utils.data import Dataset, DataLoader
|
| 14 |
+
from tqdm import tqdm
|
| 15 |
+
|
| 16 |
+
# Transformers / Datasets
|
| 17 |
+
from transformers import AutoTokenizer, get_linear_schedule_with_warmup
|
| 18 |
+
from datasets import load_dataset, DatasetDict
|
| 19 |
+
|
| 20 |
+
# Optional: Weights & Biases
|
| 21 |
+
try:
|
| 22 |
+
import wandb # noqa
|
| 23 |
+
except Exception:
|
| 24 |
+
wandb = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# =========================================================
|
| 28 |
+
# Utils
|
| 29 |
+
# =========================================================
|
| 30 |
+
def set_seed(seed: int = 1337):
|
| 31 |
+
import numpy as np
|
| 32 |
+
random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
|
| 33 |
+
if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def auto_device():
|
| 37 |
+
if torch.cuda.is_available(): return torch.device("cuda")
|
| 38 |
+
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available(): return torch.device("mps")
|
| 39 |
+
return torch.device("cpu")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def format_num(x):
|
| 43 |
+
try: return f"{x:.6g}"
|
| 44 |
+
except: return str(x)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def save_safetensors_safe(model: nn.Module, path: str, metadata: Optional[Dict[str, str]] = None):
|
| 48 |
+
"""
|
| 49 |
+
Save weights as .safetensors, handling tied weights (lm_head <- tok_emb) when needed.
|
| 50 |
+
"""
|
| 51 |
+
try:
|
| 52 |
+
from safetensors.torch import save_model # preserves shared storage & avoids duplication
|
| 53 |
+
save_model(model, path, metadata=metadata or {})
|
| 54 |
+
except Exception:
|
| 55 |
+
# Fallback that copies state_dict and de-duplicates lm_head if needed
|
| 56 |
+
try:
|
| 57 |
+
from safetensors.torch import save_file
|
| 58 |
+
state = model.state_dict()
|
| 59 |
+
if "lm_head.weight" in state and "tok_emb.weight" in state:
|
| 60 |
+
state["lm_head.weight"] = state["tok_emb.weight"].clone()
|
| 61 |
+
save_file(state, path, metadata=metadata or {})
|
| 62 |
+
except Exception as e:
|
| 63 |
+
print("[warn] safetensors not saved:", e)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# =========================================================
|
| 67 |
+
# Tokenizer helper
|
| 68 |
+
# =========================================================
|
| 69 |
+
def _gpt2_tokenizer_with_specials(
|
| 70 |
+
additional: Optional[List[str]] = None,
|
| 71 |
+
checkpoint_or_dir: Optional[str] = None,
|
| 72 |
+
) -> AutoTokenizer:
|
| 73 |
+
"""
|
| 74 |
+
If `checkpoint_or_dir` is provided, load tokenizer from there; else use 'gpt2'.
|
| 75 |
+
Ensures PAD exists (PAD→EOS), optionally adds extra specials, sets a huge model_max_length.
|
| 76 |
+
"""
|
| 77 |
+
tok = None
|
| 78 |
+
if checkpoint_or_dir is not None:
|
| 79 |
+
try:
|
| 80 |
+
tok = AutoTokenizer.from_pretrained(checkpoint_or_dir, use_fast=True)
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"[warn] Failed to load tokenizer from '{checkpoint_or_dir}': {e}")
|
| 83 |
+
print("[warn] Falling back to 'gpt2' tokenizer.")
|
| 84 |
+
|
| 85 |
+
if tok is None:
|
| 86 |
+
tok = AutoTokenizer.from_pretrained("gpt2", use_fast=True)
|
| 87 |
+
|
| 88 |
+
if tok.eos_token is None:
|
| 89 |
+
tok.add_special_tokens({"eos_token": "</s>"})
|
| 90 |
+
if tok.pad_token is None:
|
| 91 |
+
tok.pad_token = tok.eos_token
|
| 92 |
+
|
| 93 |
+
if additional:
|
| 94 |
+
new_tokens = [t for t in additional if t not in tok.get_vocab()]
|
| 95 |
+
if new_tokens:
|
| 96 |
+
tok.add_special_tokens({"additional_special_tokens": new_tokens})
|
| 97 |
+
print(f"[info] Added {len(new_tokens)} special tokens to tokenizer")
|
| 98 |
+
|
| 99 |
+
tok.model_max_length = 10_000_000
|
| 100 |
+
tok.init_kwargs["model_max_length"] = tok.model_max_length
|
| 101 |
+
return tok
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# =========================================================
|
| 105 |
+
# Fixed-block causal dataset
|
| 106 |
+
# =========================================================
|
| 107 |
+
class CausalChunked(Dataset):
|
| 108 |
+
"""Flatten tokens then slice into non-overlapping blocks; x == labels."""
|
| 109 |
+
|
| 110 |
+
def __init__(self, token_ids: Iterable[int], block_size: int):
|
| 111 |
+
ids = list(token_ids)
|
| 112 |
+
n_full = (len(ids) // block_size) * block_size
|
| 113 |
+
n_discarded = len(ids) - n_full
|
| 114 |
+
|
| 115 |
+
if n_discarded > 0 and len(ids) > 0:
|
| 116 |
+
pct = n_discarded / len(ids) * 100
|
| 117 |
+
print(f"[info] Discarded {n_discarded} tokens ({pct:.2f}%) that didn't fit into complete blocks")
|
| 118 |
+
|
| 119 |
+
ids = ids[:n_full]
|
| 120 |
+
self.blocks = [ids[i:i + block_size] for i in range(0, n_full, block_size)]
|
| 121 |
+
|
| 122 |
+
def __len__(self) -> int:
|
| 123 |
+
return len(self.blocks)
|
| 124 |
+
|
| 125 |
+
def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
|
| 126 |
+
x = torch.tensor(self.blocks[idx], dtype=torch.long)
|
| 127 |
+
return {"input_ids": x, "labels": x.clone()}
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# =========================================================
|
| 131 |
+
# PAD-mask helper (for variable-length batches with padding)
|
| 132 |
+
# =========================================================
|
| 133 |
+
def mask_pad_labels(
|
| 134 |
+
input_ids: torch.Tensor,
|
| 135 |
+
labels: torch.Tensor,
|
| 136 |
+
pad_id: Optional[int] = None,
|
| 137 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 138 |
+
) -> torch.Tensor:
|
| 139 |
+
"""
|
| 140 |
+
Clone `labels` and set pad positions to -100 (ignored by CrossEntropyLoss).
|
| 141 |
+
Prefers `attention_mask` if provided, otherwise uses pad_id to detect padding.
|
| 142 |
+
|
| 143 |
+
Note: CausalChunked produces fixed-length blocks without padding, so this is
|
| 144 |
+
only needed if you supply your own dataloader with padding.
|
| 145 |
+
"""
|
| 146 |
+
lab = labels.clone()
|
| 147 |
+
if attention_mask is not None:
|
| 148 |
+
lab[attention_mask == 0] = -100
|
| 149 |
+
elif pad_id is not None:
|
| 150 |
+
lab[input_ids == pad_id] = -100
|
| 151 |
+
return lab
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# =========================================================
|
| 155 |
+
# Dataset loader (HF datasets or local txt files)
|
| 156 |
+
# =========================================================
|
| 157 |
+
def load_dataset_fn(
|
| 158 |
+
source: str = "hf:lemonilia/wikified_english_dictionary",
|
| 159 |
+
split: str = "train",
|
| 160 |
+
*,
|
| 161 |
+
text_field: Optional[str] = "text",
|
| 162 |
+
word_field: str = "word",
|
| 163 |
+
article_field: str = "article",
|
| 164 |
+
block_size: int = 128,
|
| 165 |
+
batch_size: int = 8,
|
| 166 |
+
num_workers: int = 0,
|
| 167 |
+
shuffle: bool = True,
|
| 168 |
+
checkpoint_or_dir: Optional[str] = None,
|
| 169 |
+
additional_specials: Optional[List[str]] = None,
|
| 170 |
+
) -> Tuple[AutoTokenizer, DataLoader, Dict[str, int]]:
|
| 171 |
+
"""
|
| 172 |
+
Load and tokenize a dataset for causal LM training. Returns (tokenizer, DataLoader, meta).
|
| 173 |
+
|
| 174 |
+
source:
|
| 175 |
+
- 'hf:<name_or_path>' to read a HuggingFace dataset
|
| 176 |
+
- 'txt:/path1;/path2;...' to read local text files (semicolon-separated)
|
| 177 |
+
|
| 178 |
+
Behavior:
|
| 179 |
+
• If `text_field` is present, uses it.
|
| 180 |
+
• Else if both `word_field` and `article_field` exist, merges them as:
|
| 181 |
+
"<word>\\n<article>\\n\\n"
|
| 182 |
+
while stripping any <|begin_of_thought|>...<|end_of_thought|> spans.
|
| 183 |
+
• Else, falls back to a 'text' column if available.
|
| 184 |
+
• Appends EOS between docs/files to avoid cross-boundary contamination.
|
| 185 |
+
"""
|
| 186 |
+
tokenizer = _gpt2_tokenizer_with_specials(
|
| 187 |
+
additional=additional_specials,
|
| 188 |
+
checkpoint_or_dir=checkpoint_or_dir,
|
| 189 |
+
)
|
| 190 |
+
eos_id = tokenizer.eos_token_id
|
| 191 |
+
token_stream: List[int] = []
|
| 192 |
+
|
| 193 |
+
if source.startswith("hf:"):
|
| 194 |
+
ds_name = source[3:]
|
| 195 |
+
raw = load_dataset(ds_name)
|
| 196 |
+
if split not in raw:
|
| 197 |
+
raise ValueError(f"[error] Split '{split}' not found. Available: {list(raw.keys())}")
|
| 198 |
+
|
| 199 |
+
cols = raw[split].column_names
|
| 200 |
+
|
| 201 |
+
# A) explicit text field
|
| 202 |
+
if text_field is not None and text_field in cols:
|
| 203 |
+
field_to_use = text_field
|
| 204 |
+
|
| 205 |
+
def tok_map(batch):
|
| 206 |
+
return tokenizer(batch[field_to_use], add_special_tokens=False)
|
| 207 |
+
|
| 208 |
+
toks = raw.map(tok_map, batched=True, remove_columns=cols)
|
| 209 |
+
|
| 210 |
+
# B) merge word+article when requested/needed
|
| 211 |
+
elif (text_field is None or text_field not in cols) and word_field in cols and article_field in cols:
|
| 212 |
+
BEGIN_THOUGHT = re.compile(r"<\|begin_of_thought\|>.*?<\|end_of_thought\|>", re.DOTALL)
|
| 213 |
+
|
| 214 |
+
def fmt(batch):
|
| 215 |
+
out = []
|
| 216 |
+
for w, a in zip(batch[word_field], batch[article_field]):
|
| 217 |
+
w = (w or "").strip()
|
| 218 |
+
a = re.sub(BEGIN_THOUGHT, "", (a or "")).strip()
|
| 219 |
+
out.append(w + "\n" + a + "\n\n")
|
| 220 |
+
return {"text": out}
|
| 221 |
+
|
| 222 |
+
raw = raw.map(fmt, batched=True)
|
| 223 |
+
raw = DatasetDict({
|
| 224 |
+
sp: d.remove_columns([c for c in d.column_names if c != "text"])
|
| 225 |
+
for sp, d in raw.items()
|
| 226 |
+
})
|
| 227 |
+
|
| 228 |
+
def tok_map(batch):
|
| 229 |
+
return tokenizer(batch["text"], add_special_tokens=False)
|
| 230 |
+
|
| 231 |
+
toks = raw.map(tok_map, batched=True, remove_columns=["text"])
|
| 232 |
+
|
| 233 |
+
# C) fallback 'text'
|
| 234 |
+
elif "text" in cols:
|
| 235 |
+
def tok_map(batch):
|
| 236 |
+
return tokenizer(batch["text"], add_special_tokens=False)
|
| 237 |
+
|
| 238 |
+
toks = raw.map(tok_map, batched=True, remove_columns=cols)
|
| 239 |
+
|
| 240 |
+
else:
|
| 241 |
+
raise ValueError(
|
| 242 |
+
f"[error] Could not find a text source.\n"
|
| 243 |
+
f" - Requested text_field={text_field!r}\n"
|
| 244 |
+
f" - Available columns: {cols}\n"
|
| 245 |
+
f" - Set text_field accordingly, or set text_field=None if your dataset has "
|
| 246 |
+
f" both '{word_field}' and '{article_field}' to auto-merge."
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
n_empty = 0
|
| 250 |
+
for doc in toks[split]["input_ids"]:
|
| 251 |
+
if not doc:
|
| 252 |
+
n_empty += 1
|
| 253 |
+
continue
|
| 254 |
+
token_stream.extend(doc)
|
| 255 |
+
if eos_id is not None:
|
| 256 |
+
token_stream.append(eos_id)
|
| 257 |
+
|
| 258 |
+
if n_empty > 0:
|
| 259 |
+
print(f"[info] Skipped {n_empty} empty documents")
|
| 260 |
+
|
| 261 |
+
elif source.startswith("txt:"):
|
| 262 |
+
paths = [p for p in source[4:].split(";") if p]
|
| 263 |
+
if not paths:
|
| 264 |
+
raise ValueError("[error] No file paths provided after 'txt:'")
|
| 265 |
+
|
| 266 |
+
for p in paths:
|
| 267 |
+
if not os.path.exists(p):
|
| 268 |
+
raise FileNotFoundError(f"[error] File not found: {p}")
|
| 269 |
+
with open(p, "r", encoding="utf-8") as f:
|
| 270 |
+
text = f.read()
|
| 271 |
+
if text.strip():
|
| 272 |
+
ids = tokenizer(text, add_special_tokens=False)["input_ids"]
|
| 273 |
+
token_stream.extend(ids)
|
| 274 |
+
if eos_id is not None:
|
| 275 |
+
token_stream.append(eos_id)
|
| 276 |
+
else:
|
| 277 |
+
raise ValueError("[error] source must start with 'hf:' or 'txt:'")
|
| 278 |
+
|
| 279 |
+
if not token_stream:
|
| 280 |
+
raise ValueError("[error] No tokens extracted from the source. Check your data.")
|
| 281 |
+
|
| 282 |
+
ds = CausalChunked(token_stream, block_size)
|
| 283 |
+
if len(ds) == 0:
|
| 284 |
+
raise ValueError(
|
| 285 |
+
f"[error] Tokenized corpus ({len(token_stream)} tokens) is too small "
|
| 286 |
+
f"for block_size={block_size}. No complete blocks produced."
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
loader = DataLoader(
|
| 290 |
+
ds,
|
| 291 |
+
batch_size=batch_size,
|
| 292 |
+
shuffle=shuffle,
|
| 293 |
+
drop_last=True,
|
| 294 |
+
pin_memory=torch.cuda.is_available(),
|
| 295 |
+
num_workers=num_workers,
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
meta = {
|
| 299 |
+
"vocab_size": len(tokenizer),
|
| 300 |
+
"eos_id": eos_id,
|
| 301 |
+
"n_blocks": len(ds),
|
| 302 |
+
"n_tokens": len(token_stream),
|
| 303 |
+
"tokens_per_block": block_size,
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
print(f"[info] Dataset ready: {meta['n_blocks']} blocks, {meta['n_tokens']} tokens total")
|
| 307 |
+
return tokenizer, loader, meta
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
# =========================================================
|
| 311 |
+
# Trainer
|
| 312 |
+
# =========================================================
|
| 313 |
+
@dataclass
|
| 314 |
+
class TrainConfig:
|
| 315 |
+
output_dir: str = "outputs/hrm_run"
|
| 316 |
+
num_epochs: int = 1
|
| 317 |
+
max_steps: Optional[int] = None # if set, overrides num_epochs
|
| 318 |
+
per_device_train_batch_size: int = 8
|
| 319 |
+
gradient_accumulation_steps: int = 1
|
| 320 |
+
learning_rate: float = 1e-4
|
| 321 |
+
betas: tuple = (0.9, 0.95)
|
| 322 |
+
eps: float = 1e-8
|
| 323 |
+
weight_decay: float = 0.01
|
| 324 |
+
warmup_ratio: float = 0.06
|
| 325 |
+
max_grad_norm: float = 0.5
|
| 326 |
+
log_every: int = 100
|
| 327 |
+
save_every: int = 2000
|
| 328 |
+
eval_every: int = 2000
|
| 329 |
+
save_total_limit: int = 3
|
| 330 |
+
fp16: bool = False
|
| 331 |
+
bf16: bool = True # prefer bf16 if supported
|
| 332 |
+
seed: int = 1337
|
| 333 |
+
resume_from: Optional[str] = None # path to checkpoint dir
|
| 334 |
+
early_stopping_patience: Optional[int] = None # steps without eval improvement
|
| 335 |
+
best_metric: str = "eval/loss"
|
| 336 |
+
greater_is_better: bool = False
|
| 337 |
+
torch_compile: bool = False
|
| 338 |
+
|
| 339 |
+
# Optional W&B
|
| 340 |
+
wandb_enable: bool = False
|
| 341 |
+
wandb_entity: Optional[str] = None
|
| 342 |
+
wandb_project: Optional[str] = None
|
| 343 |
+
wandb_run_name: Optional[str] = None
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def _out_get(out: Any, key: str, default=None):
|
| 347 |
+
if isinstance(out, dict):
|
| 348 |
+
return out.get(key, default)
|
| 349 |
+
return getattr(out, key, default)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
class MiniTRLTrainer:
|
| 353 |
+
"""
|
| 354 |
+
TRL-like supervised trainer:
|
| 355 |
+
|
| 356 |
+
Model forward must accept (input_ids, labels) and return something with:
|
| 357 |
+
- loss (required)
|
| 358 |
+
- logits (optional but recommended; used for sanity checks)
|
| 359 |
+
- lm_loss (optional; logged if present)
|
| 360 |
+
- ponder_loss (optional; logged if present)
|
| 361 |
+
|
| 362 |
+
DataLoader must yield dicts with keys:
|
| 363 |
+
- "input_ids" and (optionally) "labels". If "labels" missing, labels=input_ids.
|
| 364 |
+
- If you pad to fixed length externally, also pass "attention_mask" so we can mask pad tokens.
|
| 365 |
+
"""
|
| 366 |
+
|
| 367 |
+
def __init__(
|
| 368 |
+
self,
|
| 369 |
+
model: nn.Module,
|
| 370 |
+
train_loader: DataLoader,
|
| 371 |
+
tokenizer: Optional[AutoTokenizer] = None,
|
| 372 |
+
eval_loader: Optional[DataLoader] = None,
|
| 373 |
+
config: TrainConfig = TrainConfig(),
|
| 374 |
+
compute_metrics: Optional[Callable[[Dict[str, float]], Dict[str, float]]] = None,
|
| 375 |
+
custom_loss_fn: Optional[Callable[[Any], torch.Tensor]] = None, # receives model outputs
|
| 376 |
+
device: Optional[torch.device] = None,
|
| 377 |
+
):
|
| 378 |
+
self.model = model
|
| 379 |
+
self.train_loader = train_loader
|
| 380 |
+
self.eval_loader = eval_loader
|
| 381 |
+
self.tok = tokenizer
|
| 382 |
+
self.cfg = config
|
| 383 |
+
self.compute_metrics = compute_metrics
|
| 384 |
+
self.custom_loss_fn = custom_loss_fn
|
| 385 |
+
self.device = device or auto_device()
|
| 386 |
+
set_seed(self.cfg.seed)
|
| 387 |
+
|
| 388 |
+
self.model.to(self.device)
|
| 389 |
+
if self.cfg.torch_compile:
|
| 390 |
+
try:
|
| 391 |
+
self.model = torch.compile(self.model)
|
| 392 |
+
except Exception as e:
|
| 393 |
+
print("[warn] torch.compile failed:", e)
|
| 394 |
+
|
| 395 |
+
# AMP dtype
|
| 396 |
+
if self.device.type == "cuda":
|
| 397 |
+
self.amp_dtype = torch.bfloat16 if (self.cfg.bf16 and torch.cuda.is_bf16_supported()) else (torch.float16 if self.cfg.fp16 else None)
|
| 398 |
+
else:
|
| 399 |
+
self.amp_dtype = None
|
| 400 |
+
|
| 401 |
+
# Param groups with/without weight decay
|
| 402 |
+
decay, no_decay = [], []
|
| 403 |
+
for n, p in self.model.named_parameters():
|
| 404 |
+
if not p.requires_grad:
|
| 405 |
+
continue
|
| 406 |
+
nl = n.lower()
|
| 407 |
+
if p.ndim == 1 or "norm" in nl or "bias" in nl or ("tok_emb.weight" in n):
|
| 408 |
+
no_decay.append(p)
|
| 409 |
+
else:
|
| 410 |
+
decay.append(p)
|
| 411 |
+
|
| 412 |
+
self.optimizer = torch.optim.AdamW(
|
| 413 |
+
[{"params": decay, "weight_decay": self.cfg.weight_decay},
|
| 414 |
+
{"params": no_decay, "weight_decay": 0.0}],
|
| 415 |
+
lr=self.cfg.learning_rate, betas=self.cfg.betas, eps=self.cfg.eps
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
# Scheduler
|
| 419 |
+
steps_per_epoch = math.ceil(len(self.train_loader) / max(1, self.cfg.gradient_accumulation_steps))
|
| 420 |
+
total_updates = self.cfg.max_steps if self.cfg.max_steps is not None else self.cfg.num_epochs * max(1, steps_per_epoch)
|
| 421 |
+
total_updates = max(1, total_updates) # guard
|
| 422 |
+
warmup_steps = int(self.cfg.warmup_ratio * total_updates)
|
| 423 |
+
self.scheduler = get_linear_schedule_with_warmup(self.optimizer, warmup_steps, total_updates)
|
| 424 |
+
|
| 425 |
+
# GradScaler only for fp16
|
| 426 |
+
self.scaler = torch.cuda.amp.GradScaler(enabled=(self.amp_dtype == torch.float16 and self.device.type == "cuda"))
|
| 427 |
+
|
| 428 |
+
# State
|
| 429 |
+
self.global_step = 0
|
| 430 |
+
self.best_metric_val = float("-inf") if self.cfg.greater_is_better else float("inf")
|
| 431 |
+
self.no_improve_steps = 0
|
| 432 |
+
|
| 433 |
+
os.makedirs(self.cfg.output_dir, exist_ok=True)
|
| 434 |
+
self._maybe_resume()
|
| 435 |
+
|
| 436 |
+
# W&B
|
| 437 |
+
self._wandb_run = None
|
| 438 |
+
if self.cfg.wandb_enable:
|
| 439 |
+
if wandb is None:
|
| 440 |
+
print("[warn] wandb_enable=True but wandb is not installed; proceeding without W&B.")
|
| 441 |
+
else:
|
| 442 |
+
self._wandb_run = wandb.init(
|
| 443 |
+
entity=self.cfg.wandb_entity,
|
| 444 |
+
project=self.cfg.wandb_project or "hrm",
|
| 445 |
+
name=self.cfg.wandb_run_name,
|
| 446 |
+
config=asdict(self.cfg),
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
# -------------------------- public API --------------------------
|
| 450 |
+
def train(self):
|
| 451 |
+
self.model.train()
|
| 452 |
+
log_acc_loss = 0.0
|
| 453 |
+
log_acc_tokens = 0
|
| 454 |
+
t0 = time.time()
|
| 455 |
+
|
| 456 |
+
max_updates = self.cfg.max_steps
|
| 457 |
+
if max_updates is None:
|
| 458 |
+
steps_per_epoch = math.ceil(len(self.train_loader) / max(1, self.cfg.gradient_accumulation_steps))
|
| 459 |
+
max_updates = self.cfg.num_epochs * max(1, steps_per_epoch)
|
| 460 |
+
|
| 461 |
+
pbar = tqdm(total=max_updates, initial=self.global_step, desc="Training", dynamic_ncols=True)
|
| 462 |
+
|
| 463 |
+
while self.global_step < max_updates:
|
| 464 |
+
for batch in self.train_loader:
|
| 465 |
+
if self.global_step >= max_updates:
|
| 466 |
+
break
|
| 467 |
+
|
| 468 |
+
input_ids = batch["input_ids"].to(self.device)
|
| 469 |
+
labels = batch.get("labels", input_ids).to(self.device)
|
| 470 |
+
|
| 471 |
+
# Mask pads only if attention_mask/pad present
|
| 472 |
+
pad_id = getattr(self.tok, "pad_token_id", None) if self.tok is not None else (
|
| 473 |
+
getattr(getattr(self.model, "config", None), "pad_token_id", None)
|
| 474 |
+
)
|
| 475 |
+
attn = batch.get("attention_mask", None)
|
| 476 |
+
attn = attn.to(self.device) if attn is not None else None
|
| 477 |
+
labels = mask_pad_labels(input_ids, labels, pad_id=pad_id, attention_mask=attn)
|
| 478 |
+
|
| 479 |
+
ctx = (torch.autocast(device_type=self.device.type, dtype=self.amp_dtype)
|
| 480 |
+
if (self.amp_dtype is not None and self.device.type in ("cuda", "mps"))
|
| 481 |
+
else nullcontext())
|
| 482 |
+
|
| 483 |
+
with ctx:
|
| 484 |
+
out = self.model(input_ids=input_ids, labels=labels)
|
| 485 |
+
loss = _out_get(out, "loss")
|
| 486 |
+
if self.custom_loss_fn is not None:
|
| 487 |
+
loss = self.custom_loss_fn(out)
|
| 488 |
+
loss = loss / max(1, self.cfg.gradient_accumulation_steps)
|
| 489 |
+
|
| 490 |
+
logits = _out_get(out, "logits", None)
|
| 491 |
+
if logits is not None:
|
| 492 |
+
if not torch.isfinite(logits).all():
|
| 493 |
+
mx = logits.detach().float().abs().max().item()
|
| 494 |
+
raise FloatingPointError(f"logits non-finite (max|logit|={mx})")
|
| 495 |
+
if not torch.isfinite(loss):
|
| 496 |
+
lmax = (logits.detach().float().abs().max().item() if logits is not None else float("nan"))
|
| 497 |
+
print(f"[dbg] non-finite loss; max|logit|={lmax}, lm={_out_get(out,'lm_loss')}, ponder={_out_get(out,'ponder_loss')}")
|
| 498 |
+
raise FloatingPointError("Loss became non-finite.")
|
| 499 |
+
|
| 500 |
+
if self.scaler.is_enabled():
|
| 501 |
+
self.scaler.scale(loss).backward()
|
| 502 |
+
else:
|
| 503 |
+
loss.backward()
|
| 504 |
+
|
| 505 |
+
do_step = ((self.global_step + 1) % self.cfg.gradient_accumulation_steps == 0)
|
| 506 |
+
if do_step:
|
| 507 |
+
if self.scaler.is_enabled():
|
| 508 |
+
self.scaler.unscale_(self.optimizer)
|
| 509 |
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.cfg.max_grad_norm)
|
| 510 |
+
|
| 511 |
+
if self.scaler.is_enabled():
|
| 512 |
+
prev_scale = self.scaler.get_scale()
|
| 513 |
+
self.scaler.step(self.optimizer)
|
| 514 |
+
self.scaler.update()
|
| 515 |
+
if self.scaler.get_scale() >= prev_scale:
|
| 516 |
+
self.scheduler.step()
|
| 517 |
+
else:
|
| 518 |
+
self.optimizer.step()
|
| 519 |
+
self.scheduler.step()
|
| 520 |
+
|
| 521 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 522 |
+
self.global_step += 1
|
| 523 |
+
pbar.update(1)
|
| 524 |
+
|
| 525 |
+
# Logging accumulators (token-weighted). Count only non-pad tokens.
|
| 526 |
+
tokens = int((labels != -100).sum().item())
|
| 527 |
+
lm_for_log = _out_get(out, "lm_loss", loss.detach())
|
| 528 |
+
log_acc_loss += float(lm_for_log) * max(1, tokens)
|
| 529 |
+
log_acc_tokens += max(1, tokens)
|
| 530 |
+
|
| 531 |
+
if self.global_step % max(1, self.cfg.log_every) == 0:
|
| 532 |
+
avg_loss = log_acc_loss / max(1, log_acc_tokens)
|
| 533 |
+
msg = {
|
| 534 |
+
"step": self.global_step,
|
| 535 |
+
"lr": self.scheduler.get_last_lr()[0],
|
| 536 |
+
"avg_lm_loss": avg_loss,
|
| 537 |
+
"ppl~": math.exp(min(20.0, avg_loss)),
|
| 538 |
+
"ponder": (_out_get(out, "ponder_loss", None)),
|
| 539 |
+
"elapsed_s": int(time.time() - t0),
|
| 540 |
+
}
|
| 541 |
+
tqdm.write("[log] " + ", ".join(f"{k}={format_num(v)}" for k, v in msg.items() if v is not None))
|
| 542 |
+
if self._wandb_run is not None:
|
| 543 |
+
self._wandb_run.log({k: v for k, v in msg.items() if isinstance(v, (int, float))})
|
| 544 |
+
log_acc_loss = 0.0
|
| 545 |
+
log_acc_tokens = 0
|
| 546 |
+
|
| 547 |
+
# Eval / early stop
|
| 548 |
+
if self.eval_loader and self.global_step % max(1, self.cfg.eval_every) == 0:
|
| 549 |
+
metrics = self.evaluate()
|
| 550 |
+
improved = self._check_improve(metrics[self.cfg.best_metric])
|
| 551 |
+
if self._wandb_run is not None:
|
| 552 |
+
self._wandb_run.log(metrics)
|
| 553 |
+
if self.cfg.early_stopping_patience is not None:
|
| 554 |
+
if improved:
|
| 555 |
+
self.no_improve_steps = 0
|
| 556 |
+
else:
|
| 557 |
+
self.no_improve_steps += self.cfg.eval_every
|
| 558 |
+
if self.no_improve_steps >= self.cfg.early_stopping_patience:
|
| 559 |
+
tqdm.write("[early-stop] patience exhausted.")
|
| 560 |
+
self._save_checkpoint(tag="final")
|
| 561 |
+
pbar.close()
|
| 562 |
+
return
|
| 563 |
+
|
| 564 |
+
if self.global_step % max(1, self.cfg.save_every) == 0:
|
| 565 |
+
self._save_checkpoint()
|
| 566 |
+
|
| 567 |
+
pbar.close()
|
| 568 |
+
self._save_checkpoint(tag="final")
|
| 569 |
+
|
| 570 |
+
@torch.no_grad()
|
| 571 |
+
def evaluate(self) -> Dict[str, float]:
|
| 572 |
+
self.model.eval()
|
| 573 |
+
total_loss = 0.0
|
| 574 |
+
total_tokens = 0
|
| 575 |
+
total_ponder = 0.0
|
| 576 |
+
n_batches = 0
|
| 577 |
+
|
| 578 |
+
for batch in tqdm(self.eval_loader, desc="Eval", leave=False):
|
| 579 |
+
input_ids = batch["input_ids"].to(self.device)
|
| 580 |
+
labels = batch.get("labels", input_ids).to(self.device)
|
| 581 |
+
|
| 582 |
+
pad_id = getattr(self.tok, "pad_token_id", None) if self.tok is not None else (
|
| 583 |
+
getattr(getattr(self.model, "config", None), "pad_token_id", None)
|
| 584 |
+
)
|
| 585 |
+
attn = batch.get("attention_mask", None)
|
| 586 |
+
attn = attn.to(self.device) if attn is not None else None
|
| 587 |
+
labels = mask_pad_labels(input_ids, labels, pad_id=pad_id, attention_mask=attn)
|
| 588 |
+
|
| 589 |
+
out = self.model(input_ids=input_ids, labels=labels)
|
| 590 |
+
lm = float(_out_get(out, "lm_loss", _out_get(out, "loss")))
|
| 591 |
+
tokens = int((labels != -100).sum().item())
|
| 592 |
+
|
| 593 |
+
total_loss += lm * max(1, tokens)
|
| 594 |
+
total_tokens += max(1, tokens)
|
| 595 |
+
pl = _out_get(out, "ponder_loss", None)
|
| 596 |
+
if pl is not None:
|
| 597 |
+
total_ponder += float(pl)
|
| 598 |
+
n_batches += 1
|
| 599 |
+
|
| 600 |
+
avg_loss = total_loss / max(1, total_tokens)
|
| 601 |
+
ppl = math.exp(min(20.0, avg_loss))
|
| 602 |
+
avg_ponder = (total_ponder / max(1, n_batches)) if n_batches > 0 else float("nan")
|
| 603 |
+
metrics = {"eval/loss": avg_loss, "eval/ppl": ppl, "eval/ponder": avg_ponder, "step": self.global_step}
|
| 604 |
+
tqdm.write("[eval] " + ", ".join(f"{k}={format_num(v)}" for k, v in metrics.items()))
|
| 605 |
+
self.model.train()
|
| 606 |
+
return metrics
|
| 607 |
+
|
| 608 |
+
# -------------------------- checkpoints --------------------------
|
| 609 |
+
def _save_checkpoint(self, tag: Optional[str] = None):
|
| 610 |
+
tag = tag or f"step{self.global_step}"
|
| 611 |
+
ckpt_dir = os.path.join(self.cfg.output_dir, f"ckpt-{tag}")
|
| 612 |
+
os.makedirs(ckpt_dir, exist_ok=True)
|
| 613 |
+
|
| 614 |
+
# trainer state (resumable)
|
| 615 |
+
torch.save({
|
| 616 |
+
"model": self.model.state_dict(),
|
| 617 |
+
"opt": self.optimizer.state_dict(),
|
| 618 |
+
"sched": self.scheduler.state_dict(),
|
| 619 |
+
"scaler": (self.scaler.state_dict() if self.scaler.is_enabled() else None),
|
| 620 |
+
"global_step": self.global_step,
|
| 621 |
+
"config": asdict(self.cfg),
|
| 622 |
+
}, os.path.join(ckpt_dir, "trainer_state.pt"))
|
| 623 |
+
|
| 624 |
+
# weights-only safetensors + minimal config
|
| 625 |
+
save_safetensors_safe(self.model, os.path.join(ckpt_dir, "model.safetensors"),
|
| 626 |
+
metadata={"note": "MiniTRLTrainer save", "global_step": str(self.global_step)})
|
| 627 |
+
with open(os.path.join(ckpt_dir, "config.json"), "w") as f:
|
| 628 |
+
json.dump({"global_step": self.global_step, **asdict(self.cfg)}, f, indent=2)
|
| 629 |
+
|
| 630 |
+
self._prune_checkpoints()
|
| 631 |
+
|
| 632 |
+
def _prune_checkpoints(self):
|
| 633 |
+
if self.cfg.save_total_limit is None:
|
| 634 |
+
return
|
| 635 |
+
subs = [d for d in os.listdir(self.cfg.output_dir) if d.startswith("ckpt-")]
|
| 636 |
+
if len(subs) <= self.cfg.save_total_limit:
|
| 637 |
+
return
|
| 638 |
+
subs = sorted(subs, key=lambda s: os.path.getmtime(os.path.join(self.cfg.output_dir, s)))
|
| 639 |
+
for d in subs[:-self.cfg.save_total_limit]:
|
| 640 |
+
shutil.rmtree(os.path.join(self.cfg.output_dir, d), ignore_errors=True)
|
| 641 |
+
|
| 642 |
+
def _maybe_resume(self):
|
| 643 |
+
if not self.cfg.resume_from:
|
| 644 |
+
return
|
| 645 |
+
state_path = os.path.join(self.cfg.resume_from, "trainer_state.pt")
|
| 646 |
+
if not os.path.exists(state_path):
|
| 647 |
+
print(f"[resume] not found: {state_path}")
|
| 648 |
+
return
|
| 649 |
+
ckpt = torch.load(state_path, map_location="cpu")
|
| 650 |
+
self.model.load_state_dict(ckpt["model"], strict=False)
|
| 651 |
+
self.optimizer.load_state_dict(ckpt["opt"])
|
| 652 |
+
self.scheduler.load_state_dict(ckpt["sched"])
|
| 653 |
+
if ckpt.get("scaler") and self.scaler.is_enabled():
|
| 654 |
+
self.scaler.load_state_dict(ckpt["scaler"])
|
| 655 |
+
self.global_step = int(ckpt.get("global_step", 0))
|
| 656 |
+
print(f"[resume] loaded from {self.cfg.resume_from} @ step {self.global_step}")
|
| 657 |
+
|
| 658 |
+
def _check_improve(self, val: float) -> bool:
|
| 659 |
+
improved = (val > self.best_metric_val) if self.cfg.greater_is_better else (val < self.best_metric_val)
|
| 660 |
+
if improved:
|
| 661 |
+
self.best_metric_val = val
|
| 662 |
+
return improved
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
# =========================================================
|
| 666 |
+
# Checkpoint helpers (complete save)
|
| 667 |
+
# =========================================================
|
| 668 |
+
def _state_dict_for_safetensors(model):
|
| 669 |
+
"""
|
| 670 |
+
Build a CPU state_dict suitable for safetensors.
|
| 671 |
+
If lm_head.weight is tied to tok_emb.weight, omit lm_head.weight to avoid duplicate storage.
|
| 672 |
+
"""
|
| 673 |
+
tied = hasattr(model, "lm_head") and hasattr(model, "tok_emb") and (
|
| 674 |
+
getattr(model.lm_head, "weight", None) is getattr(model.tok_emb, "weight", None)
|
| 675 |
+
)
|
| 676 |
+
sd_cpu = {k: v.detach().cpu() for k, v in model.state_dict().items()}
|
| 677 |
+
if tied and "lm_head.weight" in sd_cpu:
|
| 678 |
+
sd_cpu.pop("lm_head.weight")
|
| 679 |
+
return sd_cpu, tied
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def retie_output_embedding(model):
|
| 683 |
+
"""
|
| 684 |
+
Re-tie output and input embeddings after loading weights, if model provides get_*_embeddings().
|
| 685 |
+
"""
|
| 686 |
+
if hasattr(model, "get_input_embeddings") and hasattr(model, "get_output_embeddings"):
|
| 687 |
+
inp = model.get_input_embeddings()
|
| 688 |
+
out = model.get_output_embeddings()
|
| 689 |
+
if inp is not None and out is not None and out.weight.data_ptr() != inp.weight.data_ptr():
|
| 690 |
+
out.weight = inp.weight
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
def _chain_get(obj, attrs, default=None):
|
| 694 |
+
"""
|
| 695 |
+
Safe chained getattr: _chain_get(model, ["L_mod", "attn", "num_heads"], default=None)
|
| 696 |
+
"""
|
| 697 |
+
cur = obj
|
| 698 |
+
for a in attrs:
|
| 699 |
+
if not hasattr(cur, a):
|
| 700 |
+
return default
|
| 701 |
+
cur = getattr(cur, a)
|
| 702 |
+
return cur
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
def save_model_complete(model, save_dir, tokenizer=None, training_args=None, metadata=None):
|
| 706 |
+
"""
|
| 707 |
+
Save model with all details: weights (.pt + .safetensors), config, architecture,
|
| 708 |
+
parameter summaries, tokenizer (optional), and a README.
|
| 709 |
+
|
| 710 |
+
Returns: save_dir
|
| 711 |
+
"""
|
| 712 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 713 |
+
from datetime import datetime
|
| 714 |
+
from collections import OrderedDict
|
| 715 |
+
|
| 716 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 717 |
+
print(f"Saving model to: {save_dir}")
|
| 718 |
+
|
| 719 |
+
# 1) Weights (.pt)
|
| 720 |
+
print("1. Saving model weights (.pt)...")
|
| 721 |
+
checkpoint = {
|
| 722 |
+
"model_state_dict": model.state_dict(),
|
| 723 |
+
"timestamp": timestamp,
|
| 724 |
+
}
|
| 725 |
+
if training_args and "optimizer_state" in training_args:
|
| 726 |
+
checkpoint["optimizer_state_dict"] = training_args["optimizer_state"]
|
| 727 |
+
if training_args and "scheduler_state" in training_args:
|
| 728 |
+
checkpoint["scheduler_state_dict"] = training_args["scheduler_state"]
|
| 729 |
+
if training_args and "epoch" in training_args:
|
| 730 |
+
checkpoint["epoch"] = training_args["epoch"]
|
| 731 |
+
if training_args and "global_step" in training_args:
|
| 732 |
+
checkpoint["global_step"] = training_args["global_step"]
|
| 733 |
+
torch.save(checkpoint, os.path.join(save_dir, "model.pt"))
|
| 734 |
+
print(" ✓ Saved: model.pt")
|
| 735 |
+
|
| 736 |
+
# 1b) Weights (.safetensors)
|
| 737 |
+
print("1b. Saving model weights (.safetensors)...")
|
| 738 |
+
try:
|
| 739 |
+
from safetensors.torch import save_file
|
| 740 |
+
sd_cpu, tied = _state_dict_for_safetensors(model)
|
| 741 |
+
save_file(sd_cpu, os.path.join(save_dir, "model.safetensors"))
|
| 742 |
+
if tied:
|
| 743 |
+
print(" ℹ Weight tying detected: excluded lm_head.weight (re-tie on load).")
|
| 744 |
+
print(" ✓ Saved: model.safetensors")
|
| 745 |
+
except ImportError:
|
| 746 |
+
print(" ⚠ safetensors not installed, skipping .safetensors format")
|
| 747 |
+
except Exception as e:
|
| 748 |
+
print(f" ⚠ Could not save safetensors: {e}")
|
| 749 |
+
|
| 750 |
+
# 2) Save a minimal config (best-effort introspection)
|
| 751 |
+
print("2. Saving model config...")
|
| 752 |
+
vocab_size = getattr(model, "vocab_size", None)
|
| 753 |
+
d_model = getattr(model, "d_model", None)
|
| 754 |
+
n_heads = _chain_get(model, ["L_mod", "attn", "num_heads"], default=None)
|
| 755 |
+
d_ff = _chain_get(model, ["L_mod", "mlp", "w1", "out_features"], default=None)
|
| 756 |
+
dropout = _chain_get(model, ["L_mod", "drop", "p"], default=None)
|
| 757 |
+
k_l_steps = getattr(model, "k_l_steps", None)
|
| 758 |
+
max_cycles = getattr(model, "max_cycles", None)
|
| 759 |
+
ponder_w = getattr(model, "ponder_w", None)
|
| 760 |
+
has_out_norm = hasattr(model, "out_norm")
|
| 761 |
+
tied_flag = hasattr(model, "lm_head") and hasattr(model, "tok_emb") and (
|
| 762 |
+
getattr(model.lm_head, "weight", None) is getattr(model.tok_emb, "weight", None)
|
| 763 |
+
)
|
| 764 |
+
config = {
|
| 765 |
+
"model_type": type(model).__name__,
|
| 766 |
+
"vocab_size": vocab_size,
|
| 767 |
+
"d_model": d_model,
|
| 768 |
+
"n_heads": n_heads,
|
| 769 |
+
"d_ff": d_ff,
|
| 770 |
+
"dropout": dropout,
|
| 771 |
+
"k_l_steps": k_l_steps,
|
| 772 |
+
"max_cycles": max_cycles,
|
| 773 |
+
"ponder_loss_weight": ponder_w,
|
| 774 |
+
"has_out_norm": has_out_norm,
|
| 775 |
+
"weight_tying": tied_flag,
|
| 776 |
+
"tie_word_embeddings": tied_flag,
|
| 777 |
+
}
|
| 778 |
+
with open(os.path.join(save_dir, "config.json"), "w") as f:
|
| 779 |
+
json.dump(config, f, indent=2)
|
| 780 |
+
print(" ✓ Saved: config.json")
|
| 781 |
+
|
| 782 |
+
# 3) Architecture string
|
| 783 |
+
print("3. Saving model architecture...")
|
| 784 |
+
with open(os.path.join(save_dir, "architecture.txt"), "w") as f:
|
| 785 |
+
f.write(str(model))
|
| 786 |
+
print(" ✓ Saved: architecture.txt")
|
| 787 |
+
|
| 788 |
+
# 4) Parameter details
|
| 789 |
+
print("4. Saving parameter details...")
|
| 790 |
+
param_info = []
|
| 791 |
+
total_params = 0
|
| 792 |
+
trainable_params = 0
|
| 793 |
+
for name, p in model.named_parameters():
|
| 794 |
+
n = p.numel()
|
| 795 |
+
total_params += n
|
| 796 |
+
if p.requires_grad:
|
| 797 |
+
trainable_params += n
|
| 798 |
+
param_info.append({
|
| 799 |
+
"name": name,
|
| 800 |
+
"shape": list(p.shape),
|
| 801 |
+
"dtype": str(p.dtype),
|
| 802 |
+
"requires_grad": p.requires_grad,
|
| 803 |
+
"num_params": n,
|
| 804 |
+
"device": str(p.device),
|
| 805 |
+
})
|
| 806 |
+
param_summary = {
|
| 807 |
+
"total_parameters": total_params,
|
| 808 |
+
"trainable_parameters": trainable_params,
|
| 809 |
+
"non_trainable_parameters": total_params - trainable_params,
|
| 810 |
+
"size_mb": total_params * 4 / (1024 ** 2), # float32 estimate
|
| 811 |
+
"parameters": param_info,
|
| 812 |
+
}
|
| 813 |
+
with open(os.path.join(save_dir, "parameters.json"), "w") as f:
|
| 814 |
+
json.dump(param_summary, f, indent=2)
|
| 815 |
+
print(" ✓ Saved: parameters.json")
|
| 816 |
+
print(f" Total parameters: {total_params:,}")
|
| 817 |
+
print(f" Trainable: {trainable_params:,}")
|
| 818 |
+
print(f" Model size: {total_params * 4 / (1024**2):.2f} MB")
|
| 819 |
+
|
| 820 |
+
# 5) Layer-wise breakdown (top-level children)
|
| 821 |
+
print("5. Saving layer-wise breakdown...")
|
| 822 |
+
from collections import OrderedDict
|
| 823 |
+
layer_params = OrderedDict()
|
| 824 |
+
for name, module in model.named_children():
|
| 825 |
+
num_params = sum(p.numel() for p in module.parameters())
|
| 826 |
+
layer_params[name] = {
|
| 827 |
+
"num_params": num_params,
|
| 828 |
+
"percentage": 100 * num_params / total_params if total_params > 0 else 0,
|
| 829 |
+
}
|
| 830 |
+
with open(os.path.join(save_dir, "layer_params.json"), "w") as f:
|
| 831 |
+
json.dump(layer_params, f, indent=2)
|
| 832 |
+
print(" ✓ Saved: layer_params.json")
|
| 833 |
+
|
| 834 |
+
# 6) Training args (if provided)
|
| 835 |
+
if training_args:
|
| 836 |
+
print("6. Saving training arguments...")
|
| 837 |
+
serializable_args = {}
|
| 838 |
+
for k, v in training_args.items():
|
| 839 |
+
if isinstance(v, (int, float, str, bool, list, dict, type(None))):
|
| 840 |
+
serializable_args[k] = v
|
| 841 |
+
else:
|
| 842 |
+
serializable_args[k] = str(v)
|
| 843 |
+
with open(os.path.join(save_dir, "training_args.json"), "w") as f:
|
| 844 |
+
json.dump(serializable_args, f, indent=2)
|
| 845 |
+
print(" ✓ Saved: training_args.json")
|
| 846 |
+
|
| 847 |
+
# 7) Metadata
|
| 848 |
+
print("7. Saving metadata...")
|
| 849 |
+
metadata_full = {
|
| 850 |
+
"timestamp": timestamp,
|
| 851 |
+
"pytorch_version": torch.__version__,
|
| 852 |
+
"cuda_available": torch.cuda.is_available(),
|
| 853 |
+
"cuda_version": torch.version.cuda if torch.cuda.is_available() else None,
|
| 854 |
+
"device": str(next(model.parameters()).device),
|
| 855 |
+
"dtype": str(next(model.parameters()).dtype),
|
| 856 |
+
}
|
| 857 |
+
if metadata:
|
| 858 |
+
metadata_full.update(metadata)
|
| 859 |
+
with open(os.path.join(save_dir, "metadata.json"), "w") as f:
|
| 860 |
+
json.dump(metadata_full, f, indent=2)
|
| 861 |
+
print(" ✓ Saved: metadata.json")
|
| 862 |
+
|
| 863 |
+
# 8) Tokenizer (optional)
|
| 864 |
+
if tokenizer is not None:
|
| 865 |
+
print("8. Saving tokenizer...")
|
| 866 |
+
try:
|
| 867 |
+
tokenizer.save_pretrained(save_dir)
|
| 868 |
+
print(" ✓ Saved tokenizer files")
|
| 869 |
+
except Exception as e:
|
| 870 |
+
print(f" ⚠ Could not save tokenizer: {e}")
|
| 871 |
+
|
| 872 |
+
# 9) README
|
| 873 |
+
print("9. Creating README...")
|
| 874 |
+
readme_content = f"""# HRM/LM Model Checkpoint
|
| 875 |
+
|
| 876 |
+
## Model Information
|
| 877 |
+
- **Model Type**: {config['model_type']}
|
| 878 |
+
- **Timestamp**: {timestamp}
|
| 879 |
+
- **Total Parameters**: {total_params:,}
|
| 880 |
+
- **Trainable Parameters**: {trainable_params:,}
|
| 881 |
+
- **Model Size**: {total_params * 4 / (1024**2):.2f} MB
|
| 882 |
+
|
| 883 |
+
## Architecture (best-effort introspection)
|
| 884 |
+
- **Vocabulary Size**: {config['vocab_size']}
|
| 885 |
+
- **Hidden Dimension**: {config['d_model']}
|
| 886 |
+
- **Attention Heads**: {config['n_heads']}
|
| 887 |
+
- **FFN Dimension**: {config['d_ff']}
|
| 888 |
+
- **Dropout**: {config['dropout']}
|
| 889 |
+
- **L-mod Steps**: {config['k_l_steps']}
|
| 890 |
+
- **Max Cycles**: {config['max_cycles']}
|
| 891 |
+
- **Has Output Norm**: {config['has_out_norm']}
|
| 892 |
+
- **Weight Tying**: {config['weight_tying']} (tok_emb ↔ lm_head)
|
| 893 |
+
|
| 894 |
+
## Files
|
| 895 |
+
- `model.pt` — Full checkpoint (PyTorch)
|
| 896 |
+
- `model.safetensors` — Safetensors (excludes lm_head if tied)
|
| 897 |
+
- `config.json` — Model configuration summary
|
| 898 |
+
- `architecture.txt` — Stringified architecture
|
| 899 |
+
- `parameters.json` — Parameter metadata
|
| 900 |
+
- `layer_params.json` — Layer-wise parameter counts
|
| 901 |
+
- `training_args.json` — Training hyperparameters (if provided)
|
| 902 |
+
- `metadata.json` — Environment/device metadata
|
| 903 |
+
- Tokenizer files (if provided)
|
| 904 |
+
"""
|
| 905 |
+
with open(os.path.join(save_dir, 'README.md'), 'w') as f:
|
| 906 |
+
f.write(readme_content)
|
| 907 |
+
print(f" ✓ Saved: README.md")
|
| 908 |
+
|
| 909 |
+
print("\n" + "="*60)
|
| 910 |
+
print("SAVE COMPLETE!")
|
| 911 |
+
print("="*60)
|
| 912 |
+
print(f"Location: {save_dir}")
|
| 913 |
+
print(f"Files saved: {len(os.listdir(save_dir))}")
|
| 914 |
+
print("\nSummary:")
|
| 915 |
+
print(f" Total parameters: {total_params:,}")
|
| 916 |
+
print(f" Model size: {total_params * 4 / (1024**2):.2f} MB")
|
| 917 |
+
print(f" Config saved: ✓")
|
| 918 |
+
print(f" Weights saved: ✓")
|
| 919 |
+
print(f" Tokenizer saved: {'✓' if tokenizer else '✗'}")
|
| 920 |
+
print("="*60)
|
| 921 |
+
return save_dir
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
# =========================================================
|
| 925 |
+
# Minimal CLI (dynamic model loading via --load-fn module:function)
|
| 926 |
+
# =========================================================
|
| 927 |
+
def _load_via_callable(load_fn: str, **kwargs):
|
| 928 |
+
"""
|
| 929 |
+
load_fn: 'module.submodule:function_name' (e.g., 'hrm_utils:load_hrm')
|
| 930 |
+
kwargs: forwarded to the function
|
| 931 |
+
"""
|
| 932 |
+
if ":" not in load_fn:
|
| 933 |
+
raise ValueError("load_fn must look like 'module.submodule:function_name'")
|
| 934 |
+
mod_name, fn_name = load_fn.split(":", 1)
|
| 935 |
+
import importlib
|
| 936 |
+
mod = importlib.import_module(mod_name)
|
| 937 |
+
fn = getattr(mod, fn_name)
|
| 938 |
+
return fn(**kwargs)
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
def main():
|
| 942 |
+
import argparse
|
| 943 |
+
p = argparse.ArgumentParser(description="All-in-one HRM/LM data + trainer + checkpointing")
|
| 944 |
+
sub = p.add_subparsers(dest="cmd", required=True)
|
| 945 |
+
|
| 946 |
+
# prepare data
|
| 947 |
+
sp = sub.add_parser("prepare", help="Tokenize and build a quick dataloader")
|
| 948 |
+
sp.add_argument("--source", default="hf:lemonilia/wikified_english_dictionary")
|
| 949 |
+
sp.add_argument("--split", default="train")
|
| 950 |
+
sp.add_argument("--text-field", default="text")
|
| 951 |
+
sp.add_argument("--block-size", type=int, default=128)
|
| 952 |
+
sp.add_argument("--batch-size", type=int, default=8)
|
| 953 |
+
sp.add_argument("--tokenizer-dir", default=None)
|
| 954 |
+
|
| 955 |
+
# train
|
| 956 |
+
st = sub.add_parser("train", help="Train a model via dynamic loader")
|
| 957 |
+
st.add_argument("--load-fn", required=True, help="module:function (e.g. hrm_utils:load_hrm)")
|
| 958 |
+
st.add_argument("--load-args", default="{}", help="JSON dict of kwargs to pass to load-fn (e.g. '{\"name\":\"hrm_v0.04\",\"device\":\"cuda\",\"with_tokenizer\":true}')")
|
| 959 |
+
st.add_argument("--source", default="hf:lemonilia/wikified_english_dictionary")
|
| 960 |
+
st.add_argument("--split", default="train")
|
| 961 |
+
st.add_argument("--text-field", default="text")
|
| 962 |
+
st.add_argument("--block-size", type=int, default=128)
|
| 963 |
+
st.add_argument("--batch-size", type=int, default=8)
|
| 964 |
+
st.add_argument("--epochs", type=int, default=1)
|
| 965 |
+
st.add_argument("--lr", type=float, default=1e-4)
|
| 966 |
+
st.add_argument("--output-dir", default="outputs/hrm_run")
|
| 967 |
+
st.add_argument("--wandb", action="store_true")
|
| 968 |
+
st.add_argument("--wandb-entity", default=None)
|
| 969 |
+
st.add_argument("--wandb-project", default=None)
|
| 970 |
+
st.add_argument("--wandb-run-name", default=None)
|
| 971 |
+
|
| 972 |
+
# save checkpoint (complete)
|
| 973 |
+
ss = sub.add_parser("save", help="Save a fully-documented checkpoint for an already-loaded model")
|
| 974 |
+
ss.add_argument("--load-fn", required=True)
|
| 975 |
+
ss.add_argument("--load-args", default="{}")
|
| 976 |
+
ss.add_argument("--save-dir", default="saved_models/hrm_export")
|
| 977 |
+
ss.add_argument("--with-tokenizer", action="store_true")
|
| 978 |
+
|
| 979 |
+
args = p.parse_args()
|
| 980 |
+
|
| 981 |
+
if args.cmd == "prepare":
|
| 982 |
+
tok, loader, meta = load_dataset_fn(
|
| 983 |
+
source=args.source,
|
| 984 |
+
split=args.split,
|
| 985 |
+
text_field=args.text_field,
|
| 986 |
+
block_size=args.block_size,
|
| 987 |
+
batch_size=args.batch_size,
|
| 988 |
+
checkpoint_or_dir=args.tokenizer_dir,
|
| 989 |
+
)
|
| 990 |
+
print("[ok] Prepared one pass through dataloader:")
|
| 991 |
+
for i, b in enumerate(loader):
|
| 992 |
+
print(" batch", i, {k: v.shape for k, v in b.items()})
|
| 993 |
+
if i > 2: break
|
| 994 |
+
|
| 995 |
+
elif args.cmd == "train":
|
| 996 |
+
load_kwargs = json.loads(args.load_args or "{}")
|
| 997 |
+
obj = _load_via_callable(args.load_fn, **load_kwargs)
|
| 998 |
+
if isinstance(obj, tuple) and len(obj) >= 2:
|
| 999 |
+
model, tokenizer = obj[0], obj[1]
|
| 1000 |
+
else:
|
| 1001 |
+
# assume loader returns just model; tokenizer is optional/None
|
| 1002 |
+
model, tokenizer = obj, None
|
| 1003 |
+
|
| 1004 |
+
tok, train_loader, _ = load_dataset_fn(
|
| 1005 |
+
source=args.source,
|
| 1006 |
+
split=args.split,
|
| 1007 |
+
text_field=args.text_field,
|
| 1008 |
+
block_size=args.block_size,
|
| 1009 |
+
batch_size=args.batch_size,
|
| 1010 |
+
checkpoint_or_dir=(tokenizer.name_or_path if tokenizer is not None else None),
|
| 1011 |
+
)
|
| 1012 |
+
tokenizer = tokenizer or tok
|
| 1013 |
+
|
| 1014 |
+
cfg = TrainConfig(
|
| 1015 |
+
output_dir=args.output_dir,
|
| 1016 |
+
num_epochs=args.epochs,
|
| 1017 |
+
learning_rate=args.lr,
|
| 1018 |
+
wandb_enable=bool(args.wandb),
|
| 1019 |
+
wandb_entity=args.wandb_entity,
|
| 1020 |
+
wandb_project=args.wandb_project,
|
| 1021 |
+
wandb_run_name=args.wandb_run_name,
|
| 1022 |
+
)
|
| 1023 |
+
trainer = MiniTRLTrainer(
|
| 1024 |
+
model=model,
|
| 1025 |
+
train_loader=train_loader,
|
| 1026 |
+
tokenizer=tokenizer,
|
| 1027 |
+
eval_loader=None, # plug one in if you want
|
| 1028 |
+
config=cfg,
|
| 1029 |
+
)
|
| 1030 |
+
trainer.train()
|
| 1031 |
+
|
| 1032 |
+
elif args.cmd == "save":
|
| 1033 |
+
load_kwargs = json.loads(args.load_args or "{}")
|
| 1034 |
+
obj = _load_via_callable(args.load_fn, **load_kwargs)
|
| 1035 |
+
if isinstance(obj, tuple) and len(obj) >= 2:
|
| 1036 |
+
model, tokenizer = obj[0], obj[1]
|
| 1037 |
+
else:
|
| 1038 |
+
model, tokenizer = obj, None
|
| 1039 |
+
save_model_complete(model, args.save_dir, tokenizer=(tokenizer if args.with_tokenizer else None))
|
| 1040 |
+
|
| 1041 |
+
|
| 1042 |
+
if __name__ == "__main__":
|
| 1043 |
+
main()
|
hrm_utils.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# hrm_utils.py — Minimal, robust HRM loader + tokenizer support
|
| 2 |
+
# --------------------------------------------------------------
|
| 3 |
+
# - Handles .pt/.bin/.safetensors (single file or HF sharded index)
|
| 4 |
+
# - Adapts q/k/v names to torch.nn.MultiheadAttention format
|
| 5 |
+
# - Infers config if config.json is missing
|
| 6 |
+
# - Prefers checkpoint vocab_size over config to avoid shape mismatches
|
| 7 |
+
# - Optional tokenizer load (local files) + embedding resize + weight tying
|
| 8 |
+
# - Returns (model, tokenizer) when with_tokenizer=True (else just model)
|
| 9 |
+
|
| 10 |
+
import os, json, glob, math, inspect
|
| 11 |
+
from typing import Optional, Dict, Any
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
|
| 17 |
+
# ---------------- Blocks ----------------
|
| 18 |
+
class RMSNorm(nn.Module):
|
| 19 |
+
def __init__(self, d, eps=1e-6):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.eps = eps
|
| 22 |
+
self.weight = nn.Parameter(torch.ones(d))
|
| 23 |
+
def forward(self, x):
|
| 24 |
+
return self.weight * (x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps))
|
| 25 |
+
|
| 26 |
+
class SinusoidalPositionalEmbedding(nn.Module):
|
| 27 |
+
def __init__(self, d_model, max_len=8192):
|
| 28 |
+
super().__init__()
|
| 29 |
+
pe = torch.zeros(max_len, d_model)
|
| 30 |
+
pos = torch.arange(0, max_len).unsqueeze(1)
|
| 31 |
+
div = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
|
| 32 |
+
pe[:, 0::2] = torch.sin(pos * div)
|
| 33 |
+
pe[:, 1::2] = torch.cos(pos * div)
|
| 34 |
+
self.register_buffer("pe", pe, persistent=False)
|
| 35 |
+
def forward(self, L: int):
|
| 36 |
+
return self.pe[:L].unsqueeze(0)
|
| 37 |
+
|
| 38 |
+
class SwiGLU(nn.Module):
|
| 39 |
+
def __init__(self, d_model, d_ff, pdrop=0.1):
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.w1 = nn.Linear(d_model, d_ff, bias=False)
|
| 42 |
+
self.w2 = nn.Linear(d_model, d_ff, bias=False)
|
| 43 |
+
self.w3 = nn.Linear(d_ff, d_model, bias=False)
|
| 44 |
+
self.drop = nn.Dropout(pdrop)
|
| 45 |
+
def forward(self, x):
|
| 46 |
+
return self.drop(self.w3(F.silu(self.w1(x)) * self.w2(x)))
|
| 47 |
+
|
| 48 |
+
class AttnBlock(nn.Module):
|
| 49 |
+
def __init__(self, d_model, n_heads, d_ff, pdrop=0.1):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self.norm1 = RMSNorm(d_model)
|
| 52 |
+
self.attn = nn.MultiheadAttention(d_model, n_heads, dropout=pdrop, batch_first=True)
|
| 53 |
+
self.drop = nn.Dropout(pdrop)
|
| 54 |
+
self.norm2 = RMSNorm(d_model)
|
| 55 |
+
self.mlp = SwiGLU(d_model, d_ff, pdrop)
|
| 56 |
+
def forward(self, x, attn_mask=None, key_padding_mask=None):
|
| 57 |
+
if attn_mask is not None:
|
| 58 |
+
assert attn_mask.dtype == torch.bool and attn_mask.dim() == 2
|
| 59 |
+
if key_padding_mask is not None:
|
| 60 |
+
assert key_padding_mask.dtype == torch.bool and key_padding_mask.dim() == 2
|
| 61 |
+
h = self.norm1(x)
|
| 62 |
+
a, _ = self.attn(h, h, h, attn_mask=attn_mask, key_padding_mask=key_padding_mask, need_weights=False)
|
| 63 |
+
x = x + self.drop(a)
|
| 64 |
+
x = x + self.drop(self.mlp(self.norm2(x)))
|
| 65 |
+
return x
|
| 66 |
+
|
| 67 |
+
# ---------------- Model ----------------
|
| 68 |
+
class HRMForCausalLM(nn.Module):
|
| 69 |
+
def __init__(self, vocab_size: int, d_model=512, n_heads=8, d_ff=2048, dropout=0.1,
|
| 70 |
+
k_l_steps=4, max_cycles=8, ponder_loss_weight=1e-2):
|
| 71 |
+
super().__init__()
|
| 72 |
+
assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
|
| 73 |
+
self.vocab_size = vocab_size
|
| 74 |
+
self.d_model = d_model
|
| 75 |
+
self.k_l_steps = k_l_steps
|
| 76 |
+
self.max_cycles = max_cycles
|
| 77 |
+
self.ponder_w = ponder_loss_weight
|
| 78 |
+
|
| 79 |
+
self.tok_emb = nn.Embedding(vocab_size, d_model)
|
| 80 |
+
self.pos_emb = SinusoidalPositionalEmbedding(d_model, max_len=8192)
|
| 81 |
+
self.in_net = nn.Sequential(nn.Linear(d_model, d_model), nn.GELU(), RMSNorm(d_model))
|
| 82 |
+
|
| 83 |
+
self.L_mod = AttnBlock(d_model, n_heads, d_ff, dropout)
|
| 84 |
+
self.H_mod = AttnBlock(d_model, n_heads, d_ff, dropout)
|
| 85 |
+
|
| 86 |
+
self.halt_head = nn.Linear(d_model, 1)
|
| 87 |
+
nn.init.constant_(self.halt_head.bias, -1.5)
|
| 88 |
+
|
| 89 |
+
self.out_norm = RMSNorm(d_model)
|
| 90 |
+
|
| 91 |
+
self.lm_head = nn.Linear(d_model, vocab_size, bias=False)
|
| 92 |
+
self.lm_head.weight = self.tok_emb.weight # tie
|
| 93 |
+
|
| 94 |
+
self._cached_causal_bool = {}
|
| 95 |
+
self.apply(self._init_weights)
|
| 96 |
+
|
| 97 |
+
def _init_weights(self, m):
|
| 98 |
+
if isinstance(m, (nn.Linear, nn.Embedding)):
|
| 99 |
+
nn.init.normal_(m.weight, mean=0.0, std=0.02)
|
| 100 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 101 |
+
nn.init.zeros_(m.bias)
|
| 102 |
+
|
| 103 |
+
def _causal_bool_mask(self, L: int, device):
|
| 104 |
+
k = (L, device)
|
| 105 |
+
if k not in self._cached_causal_bool:
|
| 106 |
+
self._cached_causal_bool[k] = torch.triu(torch.ones(L, L, dtype=torch.bool, device=device), 1)
|
| 107 |
+
return self._cached_causal_bool[k]
|
| 108 |
+
|
| 109 |
+
def forward(self, input_ids, attention_mask=None, labels=None):
|
| 110 |
+
B, L = input_ids.shape
|
| 111 |
+
device = input_ids.device
|
| 112 |
+
x_tok = self.tok_emb(input_ids)
|
| 113 |
+
pos = self.pos_emb(L).to(device=device, dtype=x_tok.dtype) # keep dtype aligned
|
| 114 |
+
x = self.in_net(x_tok + pos)
|
| 115 |
+
|
| 116 |
+
causal_bool = self._causal_bool_mask(L, device)
|
| 117 |
+
key_padding_mask = (attention_mask == 0) if attention_mask is not None else None
|
| 118 |
+
|
| 119 |
+
z_L = x.clone()
|
| 120 |
+
z_H = torch.zeros_like(x)
|
| 121 |
+
|
| 122 |
+
eps = 1e-6
|
| 123 |
+
rema = torch.ones((B, L), device=device, dtype=x_tok.dtype)
|
| 124 |
+
collected_H = torch.zeros_like(z_H)
|
| 125 |
+
ponder_terms = []
|
| 126 |
+
|
| 127 |
+
for c in range(self.max_cycles):
|
| 128 |
+
for _ in range(self.k_l_steps):
|
| 129 |
+
z_L = self.L_mod(z_L + z_H + x, attn_mask=causal_bool, key_padding_mask=key_padding_mask)
|
| 130 |
+
z_H = self.H_mod(z_H + z_L, attn_mask=causal_bool, key_padding_mask=key_padding_mask)
|
| 131 |
+
|
| 132 |
+
p_halt = torch.sigmoid(self.halt_head(z_H)).squeeze(-1).clamp(eps, 1 - eps)
|
| 133 |
+
last = torch.full_like(p_halt, fill_value=(c == self.max_cycles - 1), dtype=torch.bool)
|
| 134 |
+
halt_p = torch.where(last, torch.ones_like(p_halt), p_halt)
|
| 135 |
+
|
| 136 |
+
contrib = (rema * halt_p).unsqueeze(-1)
|
| 137 |
+
collected_H = collected_H + contrib * z_H
|
| 138 |
+
|
| 139 |
+
ponder_terms.append(rema * halt_p)
|
| 140 |
+
rema = rema * (1.0 - halt_p)
|
| 141 |
+
if torch.all(rema < 1e-4):
|
| 142 |
+
break
|
| 143 |
+
|
| 144 |
+
collected_H = self.out_norm(collected_H)
|
| 145 |
+
logits = self.lm_head(collected_H)
|
| 146 |
+
|
| 147 |
+
loss = lm_loss = ponder = None
|
| 148 |
+
if labels is not None:
|
| 149 |
+
sl = logits[:, :-1, :].contiguous()
|
| 150 |
+
y = labels[:, 1:].contiguous()
|
| 151 |
+
B_, Lm1, V = sl.shape
|
| 152 |
+
lm_loss = F.cross_entropy(sl.float().view(B_ * Lm1, V), y.view(B_ * Lm1))
|
| 153 |
+
ponder = torch.stack(ponder_terms, dim=-1).sum(dim=-1).mean()
|
| 154 |
+
loss = lm_loss + self.ponder_w * ponder
|
| 155 |
+
|
| 156 |
+
return {"loss": loss, "logits": logits, "lm_loss": lm_loss, "ponder_loss": ponder}
|
| 157 |
+
|
| 158 |
+
# ---- HF-style hooks ----
|
| 159 |
+
def get_input_embeddings(self):
|
| 160 |
+
return self.tok_emb
|
| 161 |
+
def set_input_embeddings(self, new_emb):
|
| 162 |
+
self.tok_emb = new_emb
|
| 163 |
+
if hasattr(self, "lm_head"):
|
| 164 |
+
self.lm_head.weight = self.tok_emb.weight
|
| 165 |
+
def tie_weights(self):
|
| 166 |
+
if hasattr(self, "lm_head") and hasattr(self, "tok_emb"):
|
| 167 |
+
self.lm_head.weight = self.tok_emb.weight
|
| 168 |
+
|
| 169 |
+
# -------------- Loader helpers --------------
|
| 170 |
+
def _resolve_device(device: Optional[str]) -> torch.device:
|
| 171 |
+
if device is None or device == "auto":
|
| 172 |
+
if torch.cuda.is_available(): return torch.device("cuda")
|
| 173 |
+
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available(): return torch.device("mps")
|
| 174 |
+
return torch.device("cpu")
|
| 175 |
+
return torch.device(device)
|
| 176 |
+
|
| 177 |
+
def _resolve_dtype(dtype: str) -> torch.dtype:
|
| 178 |
+
d = str(dtype).lower()
|
| 179 |
+
if d in ("fp32","float32","f32"): return torch.float32
|
| 180 |
+
if d in ("bf16","bfloat16"): return torch.bfloat16
|
| 181 |
+
if d in ("fp16","float16","half"):return torch.float16
|
| 182 |
+
if d == "auto":
|
| 183 |
+
if torch.cuda.is_available() and getattr(torch.cuda, "is_bf16_supported", lambda: False)(): return torch.bfloat16
|
| 184 |
+
return torch.float32
|
| 185 |
+
raise ValueError(f"Unknown dtype {dtype}")
|
| 186 |
+
|
| 187 |
+
def _find_checkpoint(path_or_dir: str) -> str:
|
| 188 |
+
if os.path.isfile(path_or_dir): return path_or_dir
|
| 189 |
+
if not os.path.isdir(path_or_dir): raise FileNotFoundError(f"Not a file or directory: {path_or_dir}")
|
| 190 |
+
st = glob.glob(os.path.join(path_or_dir, "*.safetensors"))
|
| 191 |
+
if len(st) == 1: return st[0]
|
| 192 |
+
if len(st) > 1:
|
| 193 |
+
for cand in ("model.safetensors","pytorch_model.safetensors"):
|
| 194 |
+
p = os.path.join(path_or_dir, cand)
|
| 195 |
+
if os.path.exists(p): return p
|
| 196 |
+
return sorted(st)[0]
|
| 197 |
+
for idx in ("model.safetensors.index.json","pytorch_model.bin.index.json"):
|
| 198 |
+
p = os.path.join(path_or_dir, idx)
|
| 199 |
+
if os.path.exists(p): return p
|
| 200 |
+
for cand in ("pytorch_model.bin","model.bin","model.pt"):
|
| 201 |
+
p = os.path.join(path_or_dir, cand)
|
| 202 |
+
if os.path.exists(p): return p
|
| 203 |
+
pt = glob.glob(os.path.join(path_or_dir, "*.pt")) + glob.glob(os.path.join(path_or_dir, "*.bin"))
|
| 204 |
+
if pt: return sorted(pt)[0]
|
| 205 |
+
raise FileNotFoundError(f"No checkpoint found in {path_or_dir}")
|
| 206 |
+
|
| 207 |
+
def _torch_load(path: str):
|
| 208 |
+
try:
|
| 209 |
+
return torch.load(path, map_location="cpu", weights_only=True)
|
| 210 |
+
except TypeError:
|
| 211 |
+
return torch.load(path, map_location="cpu")
|
| 212 |
+
|
| 213 |
+
def _normalize_keys(sd: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
| 214 |
+
def strip(k: str) -> str:
|
| 215 |
+
for pref in ("module.","model.","transformer."):
|
| 216 |
+
if k.startswith(pref): return k[len(pref):]
|
| 217 |
+
return k
|
| 218 |
+
return {strip(k): v for k, v in sd.items()}
|
| 219 |
+
|
| 220 |
+
def _adapt_attention_keys(sd: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
| 221 |
+
sd = dict(sd)
|
| 222 |
+
def handle(prefix: str):
|
| 223 |
+
qkv_w = sd.pop(f"{prefix}.qkv.weight", None)
|
| 224 |
+
if qkv_w is not None:
|
| 225 |
+
sd[f"{prefix}.in_proj_weight"] = qkv_w
|
| 226 |
+
qkv_b = sd.pop(f"{prefix}.qkv.bias", None)
|
| 227 |
+
if qkv_b is not None:
|
| 228 |
+
sd[f"{prefix}.in_proj_bias"] = qkv_b
|
| 229 |
+
|
| 230 |
+
q_w = sd.pop(f"{prefix}.q_proj.weight", None)
|
| 231 |
+
k_w = sd.pop(f"{prefix}.k_proj.weight", None)
|
| 232 |
+
v_w = sd.pop(f"{prefix}.v_proj.weight", None)
|
| 233 |
+
if q_w is not None and k_w is not None and v_w is not None:
|
| 234 |
+
sd[f"{prefix}.in_proj_weight"] = torch.cat([q_w, k_w, v_w], dim=0)
|
| 235 |
+
|
| 236 |
+
q_b = sd.pop(f"{prefix}.q_proj.bias", None)
|
| 237 |
+
k_b = sd.pop(f"{prefix}.k_proj.bias", None)
|
| 238 |
+
v_b = sd.pop(f"{prefix}.v_proj.bias", None)
|
| 239 |
+
if q_b is not None and k_b is not None and v_b is not None:
|
| 240 |
+
sd[f"{prefix}.in_proj_bias"] = torch.cat([q_b, k_b, v_b], dim=0)
|
| 241 |
+
|
| 242 |
+
o_w = sd.pop(f"{prefix}.o.weight", None)
|
| 243 |
+
if o_w is not None:
|
| 244 |
+
sd[f"{prefix}.out_proj.weight"] = o_w
|
| 245 |
+
o_b = sd.pop(f"{prefix}.o.bias", None)
|
| 246 |
+
if o_b is not None:
|
| 247 |
+
sd[f"{prefix}.out_proj.bias"] = o_b
|
| 248 |
+
|
| 249 |
+
if f"{prefix}.in_proj_weight" in sd and f"{prefix}.in_proj_bias" not in sd:
|
| 250 |
+
E = sd[f"{prefix}.in_proj_weight"].shape[1]
|
| 251 |
+
sd[f"{prefix}.in_proj_bias"] = torch.zeros(3 * E, dtype=sd[f"{prefix}.in_proj_weight"].dtype)
|
| 252 |
+
for blk in ("L_mod.attn", "H_mod.attn"):
|
| 253 |
+
handle(blk)
|
| 254 |
+
return sd
|
| 255 |
+
|
| 256 |
+
def _load_state_dict(ckpt_path: str) -> Dict[str, torch.Tensor]:
|
| 257 |
+
if ckpt_path.endswith(".safetensors"):
|
| 258 |
+
from safetensors.torch import load_file as safe_load
|
| 259 |
+
return _normalize_keys(safe_load(ckpt_path, device="cpu"))
|
| 260 |
+
if ckpt_path.endswith("model.safetensors.index.json"):
|
| 261 |
+
base = os.path.dirname(ckpt_path)
|
| 262 |
+
with open(ckpt_path, "r", encoding="utf-8") as f:
|
| 263 |
+
idx = json.load(f)
|
| 264 |
+
from safetensors import safe_open
|
| 265 |
+
state = {}
|
| 266 |
+
for shard in sorted(set(idx.get("weight_map", {}).values())):
|
| 267 |
+
with safe_open(os.path.join(base, shard), framework="pt", device="cpu") as sf:
|
| 268 |
+
for k in sf.keys():
|
| 269 |
+
state[k] = sf.get_tensor(k)
|
| 270 |
+
return _normalize_keys(state)
|
| 271 |
+
if ckpt_path.endswith("pytorch_model.bin.index.json"):
|
| 272 |
+
base = os.path.dirname(ckpt_path)
|
| 273 |
+
with open(ckpt_path, "r", encoding="utf-8") as f:
|
| 274 |
+
idx = json.load(f)
|
| 275 |
+
state = {}
|
| 276 |
+
for shard in sorted(set(idx.get("weight_map", {}).values())):
|
| 277 |
+
part = _torch_load(os.path.join(base, shard))
|
| 278 |
+
if isinstance(part, dict) and "state_dict" in part:
|
| 279 |
+
part = part["state_dict"]
|
| 280 |
+
state.update(part)
|
| 281 |
+
return _normalize_keys(state)
|
| 282 |
+
if ckpt_path.endswith((".pt",".bin")):
|
| 283 |
+
obj = _torch_load(ckpt_path)
|
| 284 |
+
if isinstance(obj, dict) and "state_dict" in obj:
|
| 285 |
+
obj = obj["state_dict"]
|
| 286 |
+
return _normalize_keys(obj)
|
| 287 |
+
if ckpt_path.endswith(".json"):
|
| 288 |
+
raise ValueError("Pass the directory, not the index/config JSON.")
|
| 289 |
+
raise ValueError(f"Unsupported checkpoint type: {ckpt_path}")
|
| 290 |
+
|
| 291 |
+
def _load_config_if_any(path_or_dir: str) -> Optional[Dict[str, Any]]:
|
| 292 |
+
p = path_or_dir if path_or_dir.endswith(".json") else os.path.join(path_or_dir, "config.json")
|
| 293 |
+
if os.path.exists(p):
|
| 294 |
+
with open(p, "r", encoding="utf-8") as f:
|
| 295 |
+
return json.load(f)
|
| 296 |
+
return None
|
| 297 |
+
|
| 298 |
+
def _infer_config_from_state(sd: Dict[str, torch.Tensor]) -> Dict[str, Any]:
|
| 299 |
+
te = sd.get("tok_emb.weight", None)
|
| 300 |
+
if te is None:
|
| 301 |
+
te = sd.get("lm_head.weight", None)
|
| 302 |
+
if te is None:
|
| 303 |
+
raise ValueError("Cannot infer config: missing tok_emb.weight (or lm_head.weight).")
|
| 304 |
+
vocab_size, d_model = te.shape
|
| 305 |
+
w1 = sd.get("L_mod.mlp.w1.weight", None)
|
| 306 |
+
if w1 is None:
|
| 307 |
+
w1 = sd.get("H_mod.mlp.w1.weight", None)
|
| 308 |
+
d_ff = int(w1.shape[0]) if w1 is not None else int(4 * d_model)
|
| 309 |
+
return dict(vocab_size=int(vocab_size), d_model=int(d_model), n_heads=8, d_ff=int(d_ff),
|
| 310 |
+
dropout=0.1, k_l_steps=4, max_cycles=8, ponder_loss_weight=1e-2)
|
| 311 |
+
|
| 312 |
+
_ALLOWED_KW = {"vocab_size","d_model","n_heads","d_ff","dropout","k_l_steps","max_cycles","ponder_loss_weight"}
|
| 313 |
+
_DROP_KEYS = {"weight_tying","tie_word_embeddings","torch_dtype","architectures","model_type",
|
| 314 |
+
"initializer_range","layer_norm_eps","max_position_embeddings","use_cache"}
|
| 315 |
+
|
| 316 |
+
def _sanitize_and_map_config(raw_cfg: Dict[str, Any], ModelCls):
|
| 317 |
+
cfg = dict(raw_cfg) if raw_cfg else {}
|
| 318 |
+
for src, dst in {"hidden_size":"d_model","num_attention_heads":"n_heads","intermediate_size":"d_ff"}.items():
|
| 319 |
+
if src in cfg and dst not in cfg:
|
| 320 |
+
cfg[dst] = cfg[src]
|
| 321 |
+
if "vocab_size" not in cfg and raw_cfg and "vocab_size" in raw_cfg:
|
| 322 |
+
cfg["vocab_size"] = raw_cfg["vocab_size"]
|
| 323 |
+
for k in list(cfg.keys()):
|
| 324 |
+
if k in _DROP_KEYS:
|
| 325 |
+
cfg.pop(k, None)
|
| 326 |
+
cfg = {k: v for k, v in cfg.items() if k in _ALLOWED_KW}
|
| 327 |
+
allowed = set(inspect.signature(ModelCls.__init__).parameters.keys()) - {"self"}
|
| 328 |
+
cfg = {k: v for k, v in cfg.items() if k in allowed}
|
| 329 |
+
return cfg
|
| 330 |
+
|
| 331 |
+
def _complete_and_filter_for_model(sd: Dict[str, torch.Tensor], model: nn.Module) -> Dict[str, torch.Tensor]:
|
| 332 |
+
sd2 = dict(sd)
|
| 333 |
+
msd = model.state_dict()
|
| 334 |
+
for blk in ("L_mod.attn", "H_mod.attn"):
|
| 335 |
+
ipw = f"{blk}.in_proj_weight"
|
| 336 |
+
ipb = f"{blk}.in_proj_bias"
|
| 337 |
+
if ipw in sd2 and ipb not in sd2 and ipb in msd:
|
| 338 |
+
E = sd2[ipw].shape[1]
|
| 339 |
+
sd2[ipb] = torch.zeros(3 * E, dtype=sd2[ipw].dtype)
|
| 340 |
+
opw = f"{blk}.out_proj.weight"
|
| 341 |
+
opb = f"{blk}.out_proj.bias"
|
| 342 |
+
if opw in sd2 and opb not in sd2 and opb in msd:
|
| 343 |
+
out_dim = msd[opb].shape[0]
|
| 344 |
+
sd2[opb] = torch.zeros(out_dim, dtype=sd2[opw].dtype)
|
| 345 |
+
# Drop unknown or mismatched-shape keys
|
| 346 |
+
sd2 = {k: v for k, v in sd2.items() if (k in msd) and (tuple(v.shape) == tuple(msd[k].shape))}
|
| 347 |
+
return sd2
|
| 348 |
+
|
| 349 |
+
# -------------- Tokenizer helpers --------------
|
| 350 |
+
def _load_local_tokenizer(tok_dir: str):
|
| 351 |
+
tok = None
|
| 352 |
+
try:
|
| 353 |
+
from transformers import AutoTokenizer, PreTrainedTokenizerFast, GPT2TokenizerFast
|
| 354 |
+
try:
|
| 355 |
+
tok = AutoTokenizer.from_pretrained(tok_dir, local_files_only=True, use_fast=True, trust_remote_code=True)
|
| 356 |
+
return tok
|
| 357 |
+
except Exception as e:
|
| 358 |
+
print(f"[hrm_loader] AutoTokenizer fallback: {e}")
|
| 359 |
+
tj = os.path.join(tok_dir, "tokenizer.json")
|
| 360 |
+
if tok is None and os.path.exists(tj):
|
| 361 |
+
try:
|
| 362 |
+
from tokenizers import Tokenizer
|
| 363 |
+
core = Tokenizer.from_file(tj)
|
| 364 |
+
spec_path = os.path.join(tok_dir, "special_tokens_map.json")
|
| 365 |
+
spec = {}
|
| 366 |
+
if os.path.exists(spec_path):
|
| 367 |
+
with open(spec_path, "r", encoding="utf-8") as f:
|
| 368 |
+
spec = json.load(f)
|
| 369 |
+
tok = PreTrainedTokenizerFast(tokenizer_object=core, **{k:v for k,v in spec.items() if isinstance(v,str)})
|
| 370 |
+
return tok
|
| 371 |
+
except Exception as e:
|
| 372 |
+
print(f"[hrm_loader] tokenizer.json fallback failed: {e}")
|
| 373 |
+
vv = os.path.join(tok_dir, "vocab.json")
|
| 374 |
+
mm = os.path.join(tok_dir, "merges.txt")
|
| 375 |
+
if tok is None and os.path.exists(vv) and os.path.exists(mm):
|
| 376 |
+
try:
|
| 377 |
+
tok = GPT2TokenizerFast(vocab_file=vv, merges_file=mm)
|
| 378 |
+
spec_path = os.path.join(tok_dir, "special_tokens_map.json")
|
| 379 |
+
if os.path.exists(spec_path):
|
| 380 |
+
with open(spec_path, "r", encoding="utf-8") as f:
|
| 381 |
+
spec = json.load(f)
|
| 382 |
+
st = {k: spec[k] for k in ["bos_token","eos_token","unk_token","pad_token","sep_token","cls_token","mask_token"] if k in spec}
|
| 383 |
+
if st:
|
| 384 |
+
tok.add_special_tokens(st)
|
| 385 |
+
return tok
|
| 386 |
+
except Exception as e:
|
| 387 |
+
print(f"[hrm_loader] GPT2TokenizerFast fallback failed: {e}")
|
| 388 |
+
except Exception as e:
|
| 389 |
+
print(f"[hrm_loader] transformers/tokenizers unavailable or failed: {e}")
|
| 390 |
+
return tok
|
| 391 |
+
|
| 392 |
+
def _maybe_resize_embeddings_(model: nn.Module, vocab_size_new: int):
|
| 393 |
+
vocab_size_old = model.tok_emb.num_embeddings
|
| 394 |
+
if vocab_size_new == vocab_size_old:
|
| 395 |
+
return
|
| 396 |
+
device = next(model.parameters()).device
|
| 397 |
+
dtype = next(model.parameters()).dtype
|
| 398 |
+
d_model = model.d_model
|
| 399 |
+
old_w = model.tok_emb.weight.data.detach().to(device=device, dtype=dtype)
|
| 400 |
+
new_emb = nn.Embedding(vocab_size_new, d_model, device=device, dtype=dtype)
|
| 401 |
+
nn.init.normal_(new_emb.weight, mean=0.0, std=0.02)
|
| 402 |
+
keep = min(vocab_size_old, vocab_size_new)
|
| 403 |
+
new_emb.weight.data[:keep] = old_w[:keep]
|
| 404 |
+
model.tok_emb = new_emb
|
| 405 |
+
new_head = nn.Linear(d_model, vocab_size_new, bias=False, device=device, dtype=dtype)
|
| 406 |
+
model.lm_head = new_head
|
| 407 |
+
model.lm_head.weight = model.tok_emb.weight
|
| 408 |
+
print(f"[hrm_loader] resized embeddings: {vocab_size_old} -> {vocab_size_new}")
|
| 409 |
+
|
| 410 |
+
def _vocab_from_sd(sd: Dict[str, torch.Tensor]) -> Optional[int]:
|
| 411 |
+
te = sd.get("tok_emb.weight", None)
|
| 412 |
+
if te is None:
|
| 413 |
+
te = sd.get("lm_head.weight", None)
|
| 414 |
+
return int(te.shape[0]) if te is not None else None
|
| 415 |
+
|
| 416 |
+
# -------------- Public loader --------------
|
| 417 |
+
def load_hrm(
|
| 418 |
+
checkpoint_or_dir: str,
|
| 419 |
+
device: Optional[str] = "auto",
|
| 420 |
+
dtype: str = "auto",
|
| 421 |
+
strict: bool = True,
|
| 422 |
+
override_config: Optional[Dict[str, Any]] = None,
|
| 423 |
+
ModelCls=None,
|
| 424 |
+
with_tokenizer: bool = False,
|
| 425 |
+
tokenizer_path: Optional[str] = None,
|
| 426 |
+
):
|
| 427 |
+
if ModelCls is None:
|
| 428 |
+
ModelCls = HRMForCausalLM
|
| 429 |
+
|
| 430 |
+
ckpt = _find_checkpoint(checkpoint_or_dir)
|
| 431 |
+
sd = _load_state_dict(ckpt)
|
| 432 |
+
sd = _adapt_attention_keys(sd)
|
| 433 |
+
|
| 434 |
+
# NEW: If lm_head.weight is absent but tok_emb.weight exists (tied-weights checkpoint),
|
| 435 |
+
# mirror it to avoid "missing lm_head.weight" in load_state_dict.
|
| 436 |
+
if "lm_head.weight" not in sd and "tok_emb.weight" in sd:
|
| 437 |
+
sd["lm_head.weight"] = sd["tok_emb.weight"]
|
| 438 |
+
|
| 439 |
+
cfg_dir = checkpoint_or_dir if os.path.isdir(checkpoint_or_dir) else os.path.dirname(ckpt)
|
| 440 |
+
raw_cfg = _load_config_if_any(cfg_dir) or _infer_config_from_state(sd)
|
| 441 |
+
if override_config:
|
| 442 |
+
raw_cfg.update(override_config)
|
| 443 |
+
cfg = _sanitize_and_map_config(raw_cfg, ModelCls)
|
| 444 |
+
|
| 445 |
+
# Prefer checkpoint vocab_size to avoid size mismatches
|
| 446 |
+
sd_vocab = _vocab_from_sd(sd)
|
| 447 |
+
if sd_vocab is not None and (cfg.get("vocab_size") is None or cfg["vocab_size"] != sd_vocab):
|
| 448 |
+
print(f"[hrm_loader] adjusting vocab_size config {cfg.get('vocab_size')} -> {sd_vocab} from checkpoint")
|
| 449 |
+
cfg["vocab_size"] = sd_vocab
|
| 450 |
+
|
| 451 |
+
dev = _resolve_device(device)
|
| 452 |
+
dt = _resolve_dtype(dtype)
|
| 453 |
+
|
| 454 |
+
model = ModelCls(**cfg)
|
| 455 |
+
sd = _complete_and_filter_for_model(sd, model)
|
| 456 |
+
|
| 457 |
+
# Load weights (safe: shapes now match)
|
| 458 |
+
ik = model.load_state_dict(sd, strict=False)
|
| 459 |
+
missing = list(getattr(ik, "missing_keys", []))
|
| 460 |
+
unexpected = list(getattr(ik, "unexpected_keys", []))
|
| 461 |
+
if missing or unexpected:
|
| 462 |
+
print(f"[hrm_loader] load_state_dict: missing={len(missing)} unexpected={len(unexpected)}")
|
| 463 |
+
if missing: print(" missing (sample):", missing[:8])
|
| 464 |
+
if unexpected:print(" unexpected (sample):", unexpected[:8])
|
| 465 |
+
if strict:
|
| 466 |
+
raise RuntimeError(
|
| 467 |
+
"Strict load requested but state_dict mismatch remains.\n"
|
| 468 |
+
f"Missing (n={len(missing)}): {missing[:12]}\n"
|
| 469 |
+
f"Unexpected (n={len(unexpected)}): {unexpected[:12]}"
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
model.to(dev)
|
| 473 |
+
if dt != torch.float32:
|
| 474 |
+
model.to(dtype=dt) # parameters + buffers
|
| 475 |
+
|
| 476 |
+
try:
|
| 477 |
+
if hasattr(model, "lm_head") and hasattr(model, "tok_emb") and model.lm_head.weight is not model.tok_emb.weight:
|
| 478 |
+
model.lm_head.weight = model.tok_emb.weight
|
| 479 |
+
except Exception:
|
| 480 |
+
pass
|
| 481 |
+
|
| 482 |
+
model.eval()
|
| 483 |
+
|
| 484 |
+
tokenizer = None
|
| 485 |
+
if with_tokenizer:
|
| 486 |
+
tdir = tokenizer_path or cfg_dir
|
| 487 |
+
tokenizer = _load_local_tokenizer(tdir)
|
| 488 |
+
if tokenizer is None:
|
| 489 |
+
print(f"[hrm_loader] WARNING: could not load tokenizer from {tdir}")
|
| 490 |
+
else:
|
| 491 |
+
try:
|
| 492 |
+
_maybe_resize_embeddings_(model, len(tokenizer))
|
| 493 |
+
except Exception as e:
|
| 494 |
+
print(f"[hrm_loader] embedding resize check failed: {e}")
|
| 495 |
+
|
| 496 |
+
return (model, tokenizer) if with_tokenizer else model
|
| 497 |
+
|
| 498 |
+
__all__ = ["HRMForCausalLM", "load_hrm"]
|
hrm_utils_API_Reference.pdf
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%PDF-1.4
|
| 2 |
+
%���� ReportLab Generated PDF document http://www.reportlab.com
|
| 3 |
+
1 0 obj
|
| 4 |
+
<<
|
| 5 |
+
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R
|
| 6 |
+
>>
|
| 7 |
+
endobj
|
| 8 |
+
2 0 obj
|
| 9 |
+
<<
|
| 10 |
+
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
| 11 |
+
>>
|
| 12 |
+
endobj
|
| 13 |
+
3 0 obj
|
| 14 |
+
<<
|
| 15 |
+
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
| 16 |
+
>>
|
| 17 |
+
endobj
|
| 18 |
+
4 0 obj
|
| 19 |
+
<<
|
| 20 |
+
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
| 21 |
+
>>
|
| 22 |
+
endobj
|
| 23 |
+
5 0 obj
|
| 24 |
+
<<
|
| 25 |
+
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
| 26 |
+
>>
|
| 27 |
+
endobj
|
| 28 |
+
6 0 obj
|
| 29 |
+
<<
|
| 30 |
+
/Contents 16 0 R /MediaBox [ 0 0 612 792 ] /Parent 15 0 R /Resources <<
|
| 31 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 32 |
+
>> /Rotate 0 /Trans <<
|
| 33 |
+
|
| 34 |
+
>>
|
| 35 |
+
/Type /Page
|
| 36 |
+
>>
|
| 37 |
+
endobj
|
| 38 |
+
7 0 obj
|
| 39 |
+
<<
|
| 40 |
+
/Contents 17 0 R /MediaBox [ 0 0 612 792 ] /Parent 15 0 R /Resources <<
|
| 41 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 42 |
+
>> /Rotate 0 /Trans <<
|
| 43 |
+
|
| 44 |
+
>>
|
| 45 |
+
/Type /Page
|
| 46 |
+
>>
|
| 47 |
+
endobj
|
| 48 |
+
8 0 obj
|
| 49 |
+
<<
|
| 50 |
+
/Contents 18 0 R /MediaBox [ 0 0 612 792 ] /Parent 15 0 R /Resources <<
|
| 51 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 52 |
+
>> /Rotate 0 /Trans <<
|
| 53 |
+
|
| 54 |
+
>>
|
| 55 |
+
/Type /Page
|
| 56 |
+
>>
|
| 57 |
+
endobj
|
| 58 |
+
9 0 obj
|
| 59 |
+
<<
|
| 60 |
+
/Contents 19 0 R /MediaBox [ 0 0 612 792 ] /Parent 15 0 R /Resources <<
|
| 61 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 62 |
+
>> /Rotate 0 /Trans <<
|
| 63 |
+
|
| 64 |
+
>>
|
| 65 |
+
/Type /Page
|
| 66 |
+
>>
|
| 67 |
+
endobj
|
| 68 |
+
10 0 obj
|
| 69 |
+
<<
|
| 70 |
+
/Contents 20 0 R /MediaBox [ 0 0 612 792 ] /Parent 15 0 R /Resources <<
|
| 71 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 72 |
+
>> /Rotate 0 /Trans <<
|
| 73 |
+
|
| 74 |
+
>>
|
| 75 |
+
/Type /Page
|
| 76 |
+
>>
|
| 77 |
+
endobj
|
| 78 |
+
11 0 obj
|
| 79 |
+
<<
|
| 80 |
+
/Contents 21 0 R /MediaBox [ 0 0 612 792 ] /Parent 15 0 R /Resources <<
|
| 81 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 82 |
+
>> /Rotate 0 /Trans <<
|
| 83 |
+
|
| 84 |
+
>>
|
| 85 |
+
/Type /Page
|
| 86 |
+
>>
|
| 87 |
+
endobj
|
| 88 |
+
12 0 obj
|
| 89 |
+
<<
|
| 90 |
+
/Contents 22 0 R /MediaBox [ 0 0 612 792 ] /Parent 15 0 R /Resources <<
|
| 91 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
| 92 |
+
>> /Rotate 0 /Trans <<
|
| 93 |
+
|
| 94 |
+
>>
|
| 95 |
+
/Type /Page
|
| 96 |
+
>>
|
| 97 |
+
endobj
|
| 98 |
+
13 0 obj
|
| 99 |
+
<<
|
| 100 |
+
/PageMode /UseNone /Pages 15 0 R /Type /Catalog
|
| 101 |
+
>>
|
| 102 |
+
endobj
|
| 103 |
+
14 0 obj
|
| 104 |
+
<<
|
| 105 |
+
/Author (\(anonymous\)) /CreationDate (D:20251016093952+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20251016093952+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
| 106 |
+
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
|
| 107 |
+
>>
|
| 108 |
+
endobj
|
| 109 |
+
15 0 obj
|
| 110 |
+
<<
|
| 111 |
+
/Count 7 /Kids [ 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R ] /Type /Pages
|
| 112 |
+
>>
|
| 113 |
+
endobj
|
| 114 |
+
16 0 obj
|
| 115 |
+
<<
|
| 116 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1905
|
| 117 |
+
>>
|
| 118 |
+
stream
|
| 119 |
+
Gat=+D,8nW&H9tY(qD[VoSeV?84k@,ih2*"E0n#t-]G.G*[b0.YIhCNEoP-sm+QHL2c,u7_<gU6c9=&Ulsh-Ko_%TiPQC\FJ\t,&r#SNEn;k4,Qs@P,akk?,^KrqaHNP+.buBqTGh7u!koQ8hNAn/daAWUq?/qnsr="HfImB3](cjI@Kr*q2JtU^7_K`4r,)<MpBD@>j9$AO8LWL'o]SD-**JO'<^2)[9Nr-#8dlXPgTp+/Sr4\#cF2N'jYnU*P@[-@0A0T#-GZLW-@BQ$\'N?8%>^a;:TQA(L1)\>]6q1OG.?X4L$9,sSf_=<.=VO2;"cA%5">Wq4-=_uNBB_f?N#i>BF0/\T>15+2a2%l'J]4^h\($!@R0_LRDQ_8iLd[j(.Ib!)`e?npZ24XBs$KU65s9EX'ZQF_e)CCLRZYjtk("/oTgZ2Z#MM5"6"8!i4iN924iV(7kW/Tf$bIEm+4Y%CcLfu76W4d"^m7F@VVt1iUBdW`e?<e<jS->$j8P+E7*8,6n;G=\ZE;jj^d%(0_eR@YI.0>Pe9AT9]5&TZ#0SctUf6Pp.>0OhLr@/CbJIQE$E).[T[SE"oPk58B89Y10(U%i)9,oBm`NB`+*iO;:/o6WEM<&N#>S6_%_R=U(!?N`Mp)8R[;]1Fk]oI3m)4c#j!Q;P4_o86p]LDT'_TA@-`&,Mo][Yp's;mmBe#_4Sk_Mp_:35DP=2O?5B/go)h^s6Z<VX"#?oDI8K5n;-YetrfJG"B+o,@YCp;eZ>4=\&A80C/[SuXnd7ga\<$K>'ngt2`;g`1h[,n&XXu7<.*/kUYZAHMp`hKSeNcTnr>XDA`/nH)%O")='WDdJc\m1/dN[`5i)7%nA69^rSY=`ME=$mrN2(_@b:W[Uk)siVd5SKuk&?N@V`7$J"dlBVXON4_](>_*OQu]=!]A&GU#JLS@4-.Kt`h%gCq=aopLj<U+?ZWS6.NRE)Qju0d1u^.chmm.]r0C2'WH*X:&tBdhE6.IUHUf3S>\TB5o3H(b[AN>@(fjB.CRd'j`PFDq&f];jn2`/nO&'*>U#54`[gXsp403Z&G3?Q6_67k(ah[60U7<7"Z,%/"du<&V@Kc_^,_&H;mO4uUkAPBmos&!me`nBh(F#\%[T?Prne%_Z0o"5te(XMI%*dW]SE9?('>ZmW?aI38iH_Fu2lB\ZU5>NeSX#JbU<la%k.t,I#Q&K0QarqGKfj"E@d3OpdG1NC*k5JrG/9kO:*ej[`fn@oQV;,Vm^5CLOW:CECo+^:3G#UmYXN.=`hGGcF+Gg(gdZ5<03(OJXplV]grbOIHWW=>ORPk+:bc?GI7r2(Pm?H3VOLKD5tm+h<31U!C1JTPiADaSF%`qN4D]Xnc2DQ1KBh-g))AGM!"Ur*klLI3'^u>T>;<,Leo"/>.\9b3nldh@$_\/k.Jr8u@')>$i*f9/N(26[;hInnmA;?h`lktEke-YkJuVr0KjnqF19<J7T'%_B4;O@mc9PLT4!Jsm#EkIVYD/?5O65rbqa"\2=g&d6ZrYQpo*4L#h6!XM?O+9=O:(4gdluOEY3:HQ`L*kbeK:KH7Z],[s*eql[DC7jNXgBUK/Ha'%,?fVrOUDPpj976RKVYlOn+=o=p)H?8b?HqdT[jerSI[uE9HY(Cu@%*$:q.sWjnu1$pH9d&K)GA<?GV8#(%TkPO3ek(=uO(DSdW)h%p%RUge\6,b+paRUKNdLd]=o1+dNKOtJ/]X6<A2d=s3@2j9<<PFAViC'<fZ<G!,D#,H]M^?/6Jo"7@$/ugt<gV3>i1&S.@Y?b2+FpWhm(&mH#96Rg!>.e#b*7J#8oMucbWM.CX<4udqL)!9&L%S)(DDe%A,>#1WkZE^o6lN&/0Fa;eJl[sg$_qu8CdVS:l>IQ;6E.3?Mec_9hZ*'KDc$~>endstream
|
| 120 |
+
endobj
|
| 121 |
+
17 0 obj
|
| 122 |
+
<<
|
| 123 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1092
|
| 124 |
+
>>
|
| 125 |
+
stream
|
| 126 |
+
Gau0BgMY_1&:Ml+bZ!W!`*gG.9pLK=)Pn'G1pZt2K\#i62V;?A]0N=knXWrq#]:868B;F\4.#V#1M:koB[Gemh_55Dp7ZruJ_(#h#-[-s_!f)us6/^g>C1P=g^\G>2OVFHeg\Jj,WPrk"n0Y<eaJsb2,'HnIH268^5+JR8JDRR]pdoP+!SV]nM0g"q6&<)\U^_KUWf)p8505?#u_0tOj9?V6%Z?%H33gOrAu0@fNb\Wf-bX:[%)nTaL^A@$D0t5D0#Zp@/*;h#tO/OB-/+*qiMXmbkQ+@KI.6SGn%jh(jn?a)JH[57'MJ[@pnp^A-m&c`)<Dcf+F^H:f8H&:CtALL=;RKR>-O@iGfU.DKd0ns6NKc^0.(40%'1$18J^*1BZqH*1ih&EJ-'9-X)Ys'dS'ZC-8WM>KuKH&*O%nrg>6c4:Jo@\(j#3LW?W'eB9q>-j$%<&"VO4g(l9Nn@4!FlASA7I+.F$C/VKl!$N_u$G#n;,s*((B=ks_EP`K]Rcb?$UJ\_>8\7g7>KST3Qe47Si#$?TdVk>AHS:RI]og<$fiBK(VK3ufR#cf20hTYVZoSQ]UALlW#$_0:'P=qb?8a=b?"0&!Bo.4"dA@:+%5>qYQUT9i%B0)#<QT^%d*Y.8fo#DH`;H@?&V-TN!l\WuWs]b8,hNG>`\auuI%'Ee9k1&RZq=j:_4s+ab#QS@\Z%&6g?/\dUIbA=19NHbd[]$"MCW'HRqZJ@07`n3JWboj1gRU=5Z1FbKPPh4:[8CU8b#?XLHJ;W&0Z[k04[%nrr!Q`N/W1,!jRj*9bf>+rRB<m5@U&ro#YNPR@2NYgLrIX(;298nMs(:Kc7B?Trb2UF>Q9!\[gOpLg!+_XTW]^B&&3l;crRX/$3R%f8``9,I^B?Yl[S9DS0N.S-r=*2@_1B`a1F424"ufH7Wb#%%(QSC*PGh5(\&rji1j!@,YlJa%0h]/pN3'/M\1nOLMgg"8:"eqo=$'nVa1-!da!pE[n@?^UefHX18ub4026/eo(Dig$Y*MgpL^?AEH8W_sqogg"=V"''%-bce>dD9V7);D0$&&7XeYlM.YI1[PAIk2(>8))`[[%<n$J:~>endstream
|
| 127 |
+
endobj
|
| 128 |
+
18 0 obj
|
| 129 |
+
<<
|
| 130 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1704
|
| 131 |
+
>>
|
| 132 |
+
stream
|
| 133 |
+
Gau0DD/\/e&H88.EPO9X*ZRUeZ/'L@d'V%Q5gH[RkW[LI5Y8?-Kb@CJC6JPl*@Y-?[orlRJ2F)ta60up5'O@92Z>\uGFS_>NoB?I!QToa'HH$$@3!(\n]BArVmbr=<Z_P>&0Jk?Y1mR%R%<09:?)f@9@8q2:1&?gD=+6BI5e_M`<T:5_Lk]%`rEpN([_3?r7T`^I,J%0r-V_qW@1Z='MWI:6$@/!#KI]aDRHSO6Yg?Pf[dUd3T`_%=PnVVaYIWWP*49cpL8]N"8GIO'Z,rC9SMH$Zh?q>,j!M3f'51+P:2[^5C*>];qhLJR#6Q;o+ZY+F3!+Fahg%aM-S[AGBoTO(QVkVhdoZ@>a4LTVHa[LAm<QheRQZfWG(/Tj_rnLj>i0+@SRF7b!hWC,32)5n;4I(Ht?rGSo0X[FP$)tR<@CN>ZeS/d],cRP@:Yp/4L"F`,c\-%X4..Og/^,,9:]I@VQC+.p?IOl/gji)K[uNMMV`c0&fA-,YNXfVkhCI!cta#VE,YMku8WG(/dl<<^Z)s/2*96'B?4pUhN5\"CFQH"U2kY@^X^rlRWmV&bS!,MOcatL@$03crh,'U;PL1@9j)7Erho@:!#us!nsjfM!Z5cUb:s2.'?^9d**V_1DjE4L['SHV)4C'!"28m<"Mo3qSje=3/-R-E=uGOqa9fhe@"oHW,G;PA-$/N;CLFVVsb]D`T_J/Qn(dm;q/T1;WqT&Iq!m1jeN:UASBh+q<(8aWg!Y\1^A=C[Z@gf?AJkZM+<>4.l#Lc@hd_,_/oLo^hY33J*9(P&p4T2(n@Hml2eL'b:(r5K'M/s+)ENLD'85^0d)f$\f^$i4<%.tm3:BU.,_;=QD,..hJd]JZu+ngPj*ZI$]LpNdtL&YXXGD*/B7mXJ7&N6BcH>dMqEC$']Bq3mi/BYY>i?4;Gl-,hKrJ;h`''OkqIG4cGUS/p,&eCZg,1,qaR2Ui\A@m!G;'#ZtL47I_]&Ji_aUi#?p?RTnCHc,?@j1Tp>>XLUM!5,&R)O;RSkQ(UBodca$FUQ4SKq,h!Alpf\Z?fJCsAN>[`mQ!J/_g!2,=[[goQ#J)qhi*-/5jKWa390I-*U@2.fP0IF2KrR4V?]Ss^r5YaN4UC@f$#;UfN?s;@H&j0f.?%T#-:&iqR<gTnX%^oV)/`s>pKiu%h`HQIG.fTIM[:$1)r9E0?^gCieKANHe2k<'^b$7KrtR!Zr\5+&jI7S-82`!?N)l-I1FASOgSe.!<NTj!oT.CLX.FtS4*SurXg^N8J+Pm>Yj_Ejn$m?PA:"(_RD,dPCt3SW&D:lY6`hb_d!U9D3@?iD$,9a=+F%#[d%'te-1VY+-a+6S9BGV?#[[=R^q8*UaCt%H5>EEFjd)MbXqa0<pi*r0r>e)-=FH;=$!"-C2i$g?NpB]-s17O][XT2MQRTI*G:]Mj!<s)'2M+u3@YHjUj.`?jYFsN*N3^MGfOn9=9>-jZAC>m-LT#Y0o*UA<GYX0O\":18Ug%TcZ2L4=>:-L7WlcO?-CJ8/X7PpR?SI%L\<nush\_lscOW1P,4Y\sNi7"\l0?5Egpt-Sfr[JSTf$eM9&3";#ij[HWioUAjG?GVEtnROSDLW%kb\o"JKqfgERPPQ6]EtB>ZhUDO[Fft`9[6J*'4>_Mp/oO1oH#k>CMdH<.Ds[X8^D\[pV4&XBs&Se8^73^#ZPdX7``59=!L:IiI&5KE~>endstream
|
| 134 |
+
endobj
|
| 135 |
+
19 0 obj
|
| 136 |
+
<<
|
| 137 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 297
|
| 138 |
+
>>
|
| 139 |
+
stream
|
| 140 |
+
GarW4;+ne\'SYHA.h7B^$&_M=e*2Jq_?</8666B.Y'4_o8S]6th`&Y^@:NOJj<Wpc69$_b4K_lt]I;RMEc;&e"(/89e2p\\q+:A]9pmeoJd[(8eNV;gDO\RGq*`E"ZmQ1L4Ha75Ui488m0C#*+39HuW,JWH_#O%JiK/b$O25hJ[^>A&Y+285?.nMC;aLaj*47emC`0o(H\gg=>hPP(f9N/HW3/52N14oF_u(ClYgk0b4XhDT((;5#ropM*UjVfhJ/_UZ]8fAIFkKYm:I$"f#O7c/:#\u0qZA*na,XpY~>endstream
|
| 141 |
+
endobj
|
| 142 |
+
20 0 obj
|
| 143 |
+
<<
|
| 144 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 849
|
| 145 |
+
>>
|
| 146 |
+
stream
|
| 147 |
+
Gatn$;/b/B'SYH?'j$hN_$4UI+JhW"<2?f_d#PNe;aHO;*:QMce99P`^OEU);&V/;?&=4Ij.(0DhoQ</=!,M47iB)jJ<M\he:f"Ge8opI9].[>gc&';e+Xh(:>LL#.E_:oCY-P2Tl3@=218i/Q+DKnUXXFG:30C]1o>'qUQe8a$*tt=+#&UD4DV>Ip]\J]A+i832+)lOmPjbrL7U!Q[tY\k1$`sIh*!:=5>DAR\B(mP,bAIrGImUsmlB4Pd"?m*Y`>m8J*At/`aQksOq*$ueHQel#``J='o,@YS`[38q%/^R@g2&R3p?D.B^0UZZ&!&;bmM88&/5r.apA?;>dRr&k6'M!g+Y6^[Z($JJo6[()4'S3I*WPh%&\<GjG<E7Pb%,DXuR`oD+-$8R6uW8M,Q)0)IafrUA(+WD3EUb9n%jXNXbS)goA6ScdH"WKJ",YA4qC&K$K]!J&D`;29,U$f-NtChmGR09Cmm/)Ag!:X)AgD)/cTpYB<&$g$L$_3r''i6Wt;;^a`NA#o-@_>0d,Wp&4;__H_@.d.Y-4U5Z\`NcO6[K3T^fjRP=/LiiQ#6*7!T85+3j*05qte]A)/*I2""6%GBS;u-`g\A9Ci.nBs^N5Qh`S<<u1H3=1!LrrO6ORAaT+/.,e5&$%K/cUO+k8edblpb#=5Za.`M8p=Q$Zu^`HIIf1QEiht)WPq^,qVX6A[W_)2!oMZ#3[(QmAm"B0uHdq8sm<?liu[T]*mK6DV/PZ2RH+rG+,.rYCTt1]iU5k)Tsj`1(Vs53QS8hU][A98Vd?r6'*CXM?poG'$lDrAq?rQ0hs%BeH:9I!YDJ)[*H5HHXV6"LB8BD!G2=-56~>endstream
|
| 148 |
+
endobj
|
| 149 |
+
21 0 obj
|
| 150 |
+
<<
|
| 151 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1119
|
| 152 |
+
>>
|
| 153 |
+
stream
|
| 154 |
+
Gau1.D/Ymt&BE]&;r"E&D)3(Co$BpSd62$0enUbqb=K^*UN$C'g*TJDot0Z$-#Jf_Lhm?rC2ELV]-$5-fnKu"G5g*S!T(D,2R1*dJfB=/=:Vj-nA.9!40fr0%XQ\+L7JoNq([lX)tlaSQH.A>R$Z)F?*FQ'VNX78#$UKXkV#iM'+OE@o#27')IC5r>f0"G;U%$7aYC<7i1:ddhrrau4;d`abGFX8$U*N:*^_<9TkBm04deqh(R")A9NsaDQiMbh9D#Wb3NaK98msD:*`oj+(_glc$/!GHfuCSRe/f+/jIa@\p2fbM0n)h.:+X@Ne<hX-'ne\9a=WrWSF+;EDL_.8ZQfR?6K$Rt!8n8)"Ak!-")sMoR]"OcL"ZN)M'5KI7=`dOdM\ir8ecdCmEN$>,N9JZWAMtP`h82sGLRbOmcY46hrdZ;(][>5>`JIa=S[VmX$d"Uma.?kjSA_\<H+1uB=8kW&`X1bA^fZh2P'FDfp(LhfNdV-Rk:JVqiS?rn)(Ke&,"71bsf/TY8?:hZ3I#coXR^F!6Y)#r4>u41:.RtV(&N?abL&&'uVXt.9Em,la:Nl_UYQBME9P]G8iqPMUr5O63T2r]>\e!pLXg&eUp<*/37a3hotbs)/!]1>e_n.e9KuL;VZR[MA<(9]`q?!i\g$r)K?35(-7!q&_&UIpNQ!nHhianqX<cuOn_TPMaVSQ-[n`Ae^c\gHVV("m%nsO:*J)#VOt+/s"QN&VI075J&gW?pRB(-&[0k`E2>OjMt%l(p_"GblbF546n&i2r?KibT`,Q!dO_6;T2>]BX-=6#RE;t5FumMJhjhQmZC<JXl]s%:QuMTu/d2X(FG5u,l_JT(S\3%NfS![e;T+eYW$NIje:G(QiNoJ"\kt+k9##@WL2G"%^&/T<6kZUDLUG?jp*@XEUZ^+rRu^Vt?'?S^3NOK$=;9#I^DPEc=L-'GWS0H)>,#6:geaRRa9j1eQ7M.&EqP)r8RHsJi&HE-jUE\4YB51N+0XMg():Rc:I3C2jcj(3H0%mj;V:!&\J$B+<iRpTf45cb*EL0DLU:iaC$Sn7J>.ka!">h_V[F\c\tWE*qb'&h^f1pM9U_D^<Yk-pI'*5DKa*Qd"3+=C?i~>endstream
|
| 155 |
+
endobj
|
| 156 |
+
22 0 obj
|
| 157 |
+
<<
|
| 158 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 827
|
| 159 |
+
>>
|
| 160 |
+
stream
|
| 161 |
+
Gasammr-o6&H0m]EC-*.[a^9CUa]3;D:8,lPI`4##`@F48A+*:*)k#ZqOO0D>L[D/,f#H/h7\%$i"48Qh.P+8HqrYk.gJ7nF:3K4(6Eut0BqQlKKn=P65IC;&C[\SU%?'jlZ,=cr.IOZ%3`uI7i#e[=<-cW(95D`#3dd3hcnkXoUK>[6--2\p:D9'gJ0]E(mHsGMcXsg&22_eQnF&Z`NJc^/R?AWVUIuQg+G%dOI;H[NlaG@>:;A'4QIfsW.i(3CH@<[&#QtV<rG>O6a+,!2_,^RD&Vr+\%'c?q]Ue3F;:;%#4FAqS-@S^Fij]k,i1li/O#OlE7P\d"qBk6QBTQ1>u:d>Lf/8dW)FG!2cemi7S;1mL9c#L8>a(g&qXlqS]N=:R4]^npWmMV3)U?/:b?A.QBpV?)WrF$<Hk8p[K]fSjq2bp>>N<"l"7X"iI^s2g;'O"-1d_Mhuh#Xkc=p!X9qq'@qN=sFQm7)<$.oN&rV'kf\>;_hVft=7r^(J+\:3BSbP_Ic%s9`!_;4@,ULtKU?7iFK+/,gX20`8H/[(*G:qppkg$Z-+-'Bt)%)lI6g)AUl2`2VM$@?a>t_l`U;jW,aLp7Z%1/SRZ3oTHBC0ql5aKgXnGVg+n@.T@amUKhh$KKN]#hU!ES$[b.R!H5ZtD-GG#Htp3V9\b7FQ.X9U25<FWL)@W2RQY?U[-95L`pBa8Zakj$h$*^r#"j%\CDleG%Z/R6)=d6@#h+<E<YaU-^?E;lpp<Yck8WEU/8=Qt(3\`bV4O(rSR0,%%%=NZ&h429cja"(HelaX\(r(a4(",HXrhlMA<AH3BGH(,(;[~>endstream
|
| 162 |
+
endobj
|
| 163 |
+
xref
|
| 164 |
+
0 23
|
| 165 |
+
0000000000 65535 f
|
| 166 |
+
0000000073 00000 n
|
| 167 |
+
0000000134 00000 n
|
| 168 |
+
0000000241 00000 n
|
| 169 |
+
0000000353 00000 n
|
| 170 |
+
0000000458 00000 n
|
| 171 |
+
0000000535 00000 n
|
| 172 |
+
0000000730 00000 n
|
| 173 |
+
0000000925 00000 n
|
| 174 |
+
0000001120 00000 n
|
| 175 |
+
0000001315 00000 n
|
| 176 |
+
0000001511 00000 n
|
| 177 |
+
0000001707 00000 n
|
| 178 |
+
0000001903 00000 n
|
| 179 |
+
0000001973 00000 n
|
| 180 |
+
0000002257 00000 n
|
| 181 |
+
0000002356 00000 n
|
| 182 |
+
0000004353 00000 n
|
| 183 |
+
0000005537 00000 n
|
| 184 |
+
0000007333 00000 n
|
| 185 |
+
0000007721 00000 n
|
| 186 |
+
0000008661 00000 n
|
| 187 |
+
0000009872 00000 n
|
| 188 |
+
trailer
|
| 189 |
+
<<
|
| 190 |
+
/ID
|
| 191 |
+
[<c1487d187859f418a42d51747ddcf18d><c1487d187859f418a42d51747ddcf18d>]
|
| 192 |
+
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
| 193 |
+
|
| 194 |
+
/Info 14 0 R
|
| 195 |
+
/Root 13 0 R
|
| 196 |
+
/Size 23
|
| 197 |
+
>>
|
| 198 |
+
startxref
|
| 199 |
+
10790
|
| 200 |
+
%%EOF
|