abanm commited on
Commit
cfef4e2
·
verified ·
1 Parent(s): b2039f1

Upload 4 files

Browse files
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