symbolic_shapes.py 341 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363
  1. from __future__ import annotations
  2. import sympy
  3. from sympy import S
  4. from torch._prims_common import BoolLike, FloatLike, IntLike
  5. """
  6. ``torch.fx.experimental.symbolic_shapes`` provides interfaces for interacting with
  7. our symbolic shapes reasoning system that is used heavily in torch.compile. Although
  8. this is not generally considered public API, when writing framework code in PyTorch
  9. as well as extensions to PyTorch (e.g., in custom operator implementations), you may
  10. need to make use of these APIs to setup dynamic shapes support appropriately.
  11. """
  12. import abc
  13. import atexit
  14. import collections
  15. import dis
  16. import functools
  17. import glob
  18. import hashlib
  19. import inspect
  20. import itertools
  21. import logging
  22. import math
  23. import operator
  24. import os
  25. import re
  26. import sys
  27. import threading
  28. import traceback
  29. from collections import Counter, defaultdict
  30. from collections.abc import Callable, Generator, Iterator, Mapping, Sequence
  31. from contextlib import _GeneratorContextManager, contextmanager
  32. from dataclasses import asdict, dataclass, field
  33. from enum import Enum
  34. from typing import (
  35. Any,
  36. cast,
  37. Generic,
  38. NamedTuple,
  39. NoReturn,
  40. Optional,
  41. TYPE_CHECKING,
  42. TypeAlias,
  43. TypeGuard,
  44. TypeVar,
  45. Union,
  46. )
  47. from typing_extensions import deprecated, ParamSpec
  48. import torch
  49. import torch.fx
  50. import torch.fx.traceback as fx_traceback
  51. import torch.utils._pytree as pytree
  52. # NB: The sym_* functions are used via getattr() and must be imported here.
  53. from torch import SymBool, SymFloat, SymInt
  54. from torch._C._functorch import get_unwrapped, is_batchedtensor
  55. from torch._guards import ShapeGuard, SLoc, Source, TracingContext
  56. from torch._library.fake_class_registry import FakeScriptObject
  57. from torch._library.opaque_object import is_opaque_value
  58. from torch._logging import dtrace_structured, LazyString, structured, trace_structured
  59. from torch._subclasses.meta_utils import is_sparse_any
  60. from torch._utils_internal import signpost_event
  61. from torch.fx.experimental import _config as config
  62. from torch.fx.experimental.recording import (
  63. FakeTensorMeta,
  64. record_shapeenv_event,
  65. replay_shape_env_events,
  66. shape_env_check_state_equal,
  67. ShapeEnvEvent,
  68. )
  69. from torch.fx.experimental.sym_node import SymNode, SymTypes
  70. from torch.types import py_sym_types
  71. from torch.utils._ordered_set import OrderedSet
  72. from torch.utils._python_dispatch import is_traceable_wrapper_subclass
  73. from torch.utils._sympy.functions import (
  74. Application,
  75. CeilToInt,
  76. CleanDiv,
  77. FloorDiv,
  78. FloorToInt,
  79. IntTrueDiv,
  80. IsNonOverlappingAndDenseIndicator,
  81. Max,
  82. Mod,
  83. PythonMod,
  84. TruncToInt,
  85. )
  86. from torch.utils._sympy.numbers import int_oo
  87. from torch.utils._sympy.printers import CppPrinter, PythonPrinter
  88. from torch.utils._sympy.singleton_int import SingletonInt
  89. from torch.utils._sympy.solve import try_solve
  90. from torch.utils._sympy.symbol import make_symbol, symbol_is_type, SymT
  91. from torch.utils._sympy.value_ranges import (
  92. bound_sympy,
  93. SymPyValueRangeAnalysis,
  94. ValueRangeError,
  95. ValueRanges,
  96. )
  97. from torch.utils._traceback import CapturedTraceback, format_frame
  98. if TYPE_CHECKING:
  99. import types
  100. from torch import Tensor
  101. from torch._dynamo.source import TensorPropertySource
  102. from torch._subclasses.fake_tensor import FakeTensor
  103. from torch.types import BoolLikeType, FloatLikeType, IntLikeType
  104. InputList = list
  105. DimList = list
  106. log = logging.getLogger(__name__)
  107. class GuardOnDataDependentSymNode(RuntimeError):
  108. cond: sympy.Basic
  109. def __init__(self, cond: sympy.Basic, *args: Any) -> None:
  110. super().__init__(*args)
  111. self.cond = cond
  112. class PendingUnbackedSymbolNotFound(RuntimeError):
  113. pass
  114. aten = torch._ops.ops.aten # type: ignore[has-type]
  115. __all__ = [
  116. "size_hint",
  117. "guard_or_false",
  118. "guard_or_true",
  119. "has_symbolic_sizes_strides",
  120. "create_contiguous",
  121. "ShapeEnv",
  122. "is_concrete_int",
  123. "is_concrete_float",
  124. "is_concrete_bool",
  125. "has_static_value",
  126. "guard_int",
  127. "guard_float",
  128. "guard_scalar",
  129. "canonicalize_bool_expr",
  130. "hint_int",
  131. "SYMPY_INTERP",
  132. "free_symbols",
  133. "is_symbol_binding_fx_node",
  134. "is_nested_int",
  135. "SHAPEENV_EVENT_KEY",
  136. "CURRENT_NODE_KEY",
  137. "has_free_symbols",
  138. "has_free_unbacked_symbols",
  139. "sym_and",
  140. "sym_eq",
  141. "sym_or",
  142. "SymbolicContext",
  143. "StatelessSymbolicContext",
  144. "StatefulSymbolicContext",
  145. "SubclassSymbolicContext",
  146. "SymIntSymbolicContext",
  147. "TrackedFake",
  148. "statically_known_true",
  149. "statically_known_false",
  150. "guard_size_oblivious",
  151. "check_consistent",
  152. "compute_unbacked_bindings",
  153. "ConvertIntKey",
  154. "rebind_unbacked",
  155. "resolve_unbacked_bindings",
  156. "is_accessor_node",
  157. "ValueRangesSLoc",
  158. "SymIntEqByExpr",
  159. "Specialization",
  160. ]
  161. # FX node metadata keys for symbolic shape FX graph.
  162. SHAPEENV_EVENT_KEY = "shapeenv_event"
  163. CURRENT_NODE_KEY = "current_node"
  164. def log_lru_cache_stats(wrapped_f: functools._lru_cache_wrapper[object]) -> None:
  165. log.debug(
  166. "lru_cache_stats %s: %s",
  167. wrapped_f.__name__, # type: ignore[attr-defined]
  168. wrapped_f.cumulative_cache_info(), # type: ignore[attr-defined]
  169. )
  170. # Note about Sympy Expr/SympyBoolean/Basic typing: the Sympy hierarchy is
  171. #
  172. # Basic
  173. # Expr
  174. # SympyBoolean
  175. # Relational
  176. #
  177. # Notably, Expr and SympyBoolean are not related. So use Basic when the
  178. # expression could denote int, float OR bool, and otherwise use the more
  179. # specific Expr for int/float and SympyBoolean for bool.
  180. #
  181. # In obscure Meta only situations, sympy.logic.boolalg doesn't exist at runtime.
  182. # So make sure only type checker evaluates this alias.
  183. # Xref: https://www.internalfb.com/diff/D53324783
  184. SympyBoolean: TypeAlias = "sympy.logic.boolalg.Boolean"
  185. _T = TypeVar("_T")
  186. _SympyT = TypeVar("_SympyT", sympy.Expr, SympyBoolean, sympy.Basic)
  187. class SymIntEqByExpr:
  188. """
  189. This is a wrapper around SymInt which has alternative semantics for
  190. equality and pickling. Specifically, instead of erroring or guarding, we
  191. instead will hash/compare equality based on the underlying sympy
  192. expression; e.g., s0 and s1 will always compare as False.
  193. NB: This does NOT do fancy analysis that maybe_evaluate_static does;
  194. we can only reason through equalities that occur because to expressions
  195. canonicalize to the same expression via regular simplification.
  196. """
  197. @staticmethod
  198. def _extract(val: Union[torch.SymInt, int]) -> sympy.Expr:
  199. if isinstance(val, torch.SymInt):
  200. return val.node.expr
  201. else:
  202. return sympy.Integer(val)
  203. def __init__(self, val: Union[torch.SymInt, int]) -> None:
  204. self.val: sympy.Expr = SymIntEqByExpr._extract(val)
  205. def __repr__(self) -> str:
  206. return repr(self.val)
  207. def __eq__(self, other: object) -> bool:
  208. if not isinstance(other, SymIntEqByExpr):
  209. raise AssertionError(f"Expected SymIntEqByExpr, got {type(other)}")
  210. return self.val == other.val
  211. def __hash__(self) -> int:
  212. return hash(self.val)
  213. def _nested_int_aware_sort(
  214. tup: tuple[IntLikeType, int],
  215. ) -> tuple[int, IntLikeType, int]:
  216. return (
  217. # Order nested ints by their coefficients.
  218. # 1 here to order nested ints after non-nested-ints.
  219. (1, tup[0].node.nested_int_coeff(), tup[1])
  220. if is_nested_int(tup[0])
  221. else (0, *tup)
  222. )
  223. # Wrapper on lru_cache that reports statistics at process end
  224. def lru_cache(
  225. maxsize: Optional[int],
  226. ) -> Callable[[Callable[..., _T]], functools._lru_cache_wrapper[_T]]:
  227. def inner(f: Callable[..., _T]) -> functools._lru_cache_wrapper[_T]:
  228. wrapped_f = functools.lru_cache(maxsize)(f)
  229. old_cache_clear = wrapped_f.cache_clear
  230. prev_hits = 0
  231. prev_misses = 0
  232. # TODO: There's a ref-cycle here (wrapped_f -> cumulative_cache_info
  233. # -> wrapped_f) but cannot be solved with weakref as wrapped_f is not
  234. # weakref'able on some versions of Python
  235. def cumulative_cache_info() -> functools._CacheInfo:
  236. cur = wrapped_f.cache_info()
  237. return functools._CacheInfo(
  238. prev_hits + cur.hits,
  239. prev_misses + cur.misses,
  240. cur.maxsize,
  241. cur.currsize,
  242. )
  243. def new_cache_clear() -> None:
  244. nonlocal prev_hits, prev_misses
  245. cur = wrapped_f.cache_info()
  246. prev_hits += cur.hits
  247. prev_misses += cur.misses
  248. old_cache_clear()
  249. wrapped_f.cache_clear = new_cache_clear # type: ignore[attr-defined, method-assign]
  250. wrapped_f.cumulative_cache_info = cumulative_cache_info # type: ignore[attr-defined, method-assign]
  251. if log.isEnabledFor(logging.DEBUG):
  252. atexit.register(log_lru_cache_stats, wrapped_f) # type: ignore[arg-type]
  253. return wrapped_f
  254. return inner
  255. # These are modules that contain generic code for interacting with ShapeEnv
  256. # which are unlikely to identify a particular interesting guard statement
  257. @lru_cache(None)
  258. def uninteresting_files() -> set[str]:
  259. import torch._compile
  260. import torch._dynamo.eval_frame
  261. import torch._higher_order_ops
  262. import torch._inductor.sizevars
  263. import torch._library.custom_ops
  264. import torch._library.fake_impl
  265. import torch._logging
  266. import torch._subclasses.fake_tensor
  267. import torch._subclasses.meta_utils
  268. import torch.export._trace
  269. mods = [
  270. sys.modules[__name__],
  271. torch.export._trace,
  272. torch.fx.experimental.recording,
  273. torch.fx.experimental.sym_node,
  274. torch.fx.interpreter,
  275. torch.fx._symbolic_trace,
  276. torch,
  277. torch._compile,
  278. torch._dynamo.eval_frame,
  279. torch._inductor.sizevars,
  280. torch._library.custom_ops,
  281. torch._library.fake_impl,
  282. torch._subclasses.meta_utils,
  283. torch._subclasses.fake_tensor,
  284. torch._logging._internal,
  285. torch._logging.structured,
  286. ]
  287. import torch._dynamo.guards
  288. files = {inspect.getfile(m) for m in mods}
  289. # Add all Python files in torch._higher_order_ops directory
  290. higher_order_ops_dir = os.path.dirname(torch._higher_order_ops.__file__)
  291. hop_files = glob.glob(os.path.join(higher_order_ops_dir, "*.py"))
  292. return (
  293. files
  294. | set(hop_files)
  295. | torch._dynamo.guards.uninteresting_files()
  296. | {"<string>"}
  297. )
  298. class ConstraintViolationError(RuntimeError):
  299. pass
  300. def has_symbolic_sizes_strides(elem: torch.Tensor) -> bool:
  301. return elem._has_symbolic_sizes_strides
  302. Int: TypeAlias = Union[torch.SymInt, int]
  303. def create_contiguous(shape: Sequence[Int]) -> list[Int]:
  304. strides: list[Int] = [1]
  305. for dim in reversed(shape[:-1]):
  306. strides.append(dim * strides[-1]) # type: ignore[operator]
  307. return list(reversed(strides))
  308. @deprecated("used size_hint instead of hint_int", category=FutureWarning)
  309. def hint_int(a: Union[torch.SymInt, int], fallback: Optional[int] = None) -> int:
  310. return size_hint(a, fallback)
  311. def size_hint(a: Union[torch.SymInt, int], fallback: Optional[int] = None) -> int:
  312. """
  313. Retrieve the hint for an int (based on the underlying real values as observed
  314. at runtime). If no hint is available (e.g., because data dependent shapes),
  315. if fallback is not None, use that instead to hint each unbacked symbol individually
  316. (otherwise raise an error).
  317. """
  318. if isinstance(a, torch.SymInt):
  319. return a.node.require_hint(fallback)
  320. if type(a) is not int:
  321. raise AssertionError(f"Expected int, got {type(a)}")
  322. return a
  323. Scalar: TypeAlias = Union[torch.SymInt, torch.SymFloat, torch.SymBool, int, float, bool]
  324. def has_hint(a: Scalar) -> bool:
  325. if isinstance(a, SymTypes):
  326. return a.node.has_hint()
  327. return True
  328. def is_concrete_int(a: IntLikeType) -> bool:
  329. """
  330. Utility to check if underlying object
  331. in SymInt is concrete value. Also returns
  332. true if integer is passed in.
  333. Args:
  334. a (SymInt or int): Object to test if it int
  335. """
  336. if not isinstance(a, (SymInt, int)):
  337. raise AssertionError(f"Expected SymInt or int, got {type(a)}")
  338. if isinstance(a, int):
  339. return True
  340. if isinstance(a.node.expr, sympy.core.numbers.Integer):
  341. return True
  342. return False
  343. def is_concrete_float(a: FloatLikeType) -> bool:
  344. r"""Utility to check if underlying object
  345. in SymInt is concrete value. Also returns
  346. true if integer is passed in.
  347. Args:
  348. a (SymInt or float): Object to test if it float
  349. """
  350. if not isinstance(a, (SymFloat, float)):
  351. raise AssertionError(f"Expected SymFloat or float, got {type(a)}")
  352. if isinstance(a, float):
  353. return True
  354. if isinstance(a.node.expr, sympy.core.numbers.Float):
  355. return True
  356. return False
  357. def is_concrete_bool(a: BoolLikeType) -> bool:
  358. """
  359. Utility to check if underlying object
  360. in SymBool is concrete value. Also returns
  361. true if integer is passed in.
  362. Args:
  363. a (SymBool or bool): Object to test if it bool
  364. """
  365. if not isinstance(a, (SymBool, bool)):
  366. raise AssertionError(f"Expected SymBool or bool, got {type(a)}")
  367. if isinstance(a, bool):
  368. return True
  369. if isinstance(
  370. a.node.expr, (sympy.logic.boolalg.BooleanTrue, sympy.logic.boolalg.BooleanFalse)
  371. ):
  372. return True
  373. return False
  374. def has_static_value(a: Union[SymBool, SymFloat, SymInt, bool, float, int]) -> bool:
  375. """
  376. User-code friendly utility to check if a value is static or dynamic.
  377. Returns true if given a constant, or a symbolic expression with a fixed value.
  378. Args:
  379. a (Union[SymBool, SymFloat, SymInt, bool, float, int]): Object to test
  380. """
  381. if not isinstance(a, BoolLike + FloatLike + IntLike):
  382. raise AssertionError(f"Expected BoolLike/FloatLike/IntLike, got {type(a)}")
  383. if (
  384. isinstance(a, BoolLike)
  385. and is_concrete_bool(a) # type: ignore[arg-type]
  386. or isinstance(a, FloatLike)
  387. and is_concrete_float(a) # type: ignore[arg-type]
  388. or isinstance(a, IntLike)
  389. and is_concrete_int(a) # type: ignore[arg-type]
  390. ):
  391. return True
  392. if not isinstance(a, py_sym_types):
  393. raise AssertionError(f"Expected py_sym_types, got {type(a)}")
  394. return a.node.shape_env.bound_sympy(a.node.expr).is_singleton() # type: ignore[union-attr]
  395. @deprecated(
  396. "guard_size_oblivious will be removed. Consider using explicit unbacked handling \
  397. potentially utilizing guard_or_false, guard_or_true, or statically_known_true",
  398. category=FutureWarning,
  399. )
  400. def guard_size_oblivious(expr: Union[torch.SymBool, bool]) -> bool:
  401. """
  402. Perform a guard on a symbolic boolean expression in a size oblivious way.
  403. This is typically used when a non-oblivious test would result in a guard
  404. on a data dependent value of which we don't know the value of at compile time.
  405. When a guard is tested this way, we may diverge in behavior from how regular
  406. PyTorch semantics would treat it. For more information, see
  407. https://github.com/pytorch/pytorch/pull/118579
  408. """
  409. if isinstance(expr, torch.SymBool):
  410. return expr.node.guard_size_oblivious("", 0)
  411. else:
  412. if not isinstance(expr, bool):
  413. raise AssertionError(f"Expected bool, got {type(expr)}")
  414. return expr
  415. def check_consistent(new: _T, old: _T) -> None:
  416. """
  417. Test that two "meta" values (typically either Tensor or SymInt) have
  418. the same values, e.g., after retracing. If we don't understand the
  419. quantities in question, we'll just skip the consistency check.
  420. """
  421. # TODO: do boolean equality test too, see
  422. # https://github.com/pytorch/pytorch/issues/124110
  423. scalar_types = (torch.SymInt, torch.SymFloat, int, float)
  424. if isinstance(new, torch.Tensor):
  425. if not isinstance(old, torch.Tensor):
  426. raise AssertionError(f"Expected Tensor, got {type(old)}")
  427. torch._check(
  428. old.dim() == new.dim(), lambda: f"{old.shape} != {new.shape} (old != new)"
  429. )
  430. # Do this manually so that each individual test is irrefutable
  431. # (TODO: should be a helper for this, maybe sym_eq? That
  432. # gives us a compound expression and I'm not sure it
  433. # simplifies right now)
  434. for i, j in zip(old.shape, new.shape):
  435. torch._check(i == j, lambda: f"{old.shape} != {new.shape} (old != new)")
  436. # NB: bool is subclass of int
  437. elif isinstance(new, scalar_types) and not isinstance(new, bool):
  438. if not (isinstance(old, scalar_types) and not isinstance(old, bool)):
  439. raise AssertionError(f"{old} != {new}")
  440. torch._check(old == new, lambda: f"{old} != {new} (old != new)")
  441. def resolve_unbacked_bindings(
  442. shape_env: Optional[ShapeEnv],
  443. bindings: Optional[dict[sympy.Symbol, pytree.KeyPath]],
  444. ) -> Optional[dict[sympy.Symbol, pytree.KeyPath]]:
  445. """
  446. When we do fake tensor prop, we oftentimes will allocate new unbacked symints.
  447. We then run proxy tensor mode, which populates node.meta["unbacked_bindings"]
  448. with these new symints. To ensure consistency we use PropagateUnbackedSymInts
  449. to rename unbacked bindings to their old ones. But all of the node metas are
  450. still using the old bindings from before the renaming. This function helps to
  451. post facto apply any renamings discovered in the PropagateUnbackedSymInts pass.
  452. """
  453. if bindings is None:
  454. return None
  455. if shape_env is None:
  456. raise AssertionError("shape_env should not be None")
  457. return {shape_env.unbacked_renamings.get(k, k): v for k, v in bindings.items()}
  458. Result: TypeAlias = Union[torch.Tensor, tuple[torch.Tensor, ...]]
  459. def rebind_unbacked(
  460. shape_env: Optional[ShapeEnv], n: torch.fx.Node, result: Result
  461. ) -> None:
  462. """
  463. Suppose we are retracing a pre-existing FX graph that previously had
  464. fake tensor propagation (and therefore unbacked SymInts). When we retrace,
  465. we re-propagate fake tensors, which results in new unbacked SymInts.
  466. When this happens, we need to tell the shape environment about the equivalence
  467. of the old and new unbacked SymInts. Pass us the old torch.fx.Node (which
  468. has the old binding information) and the new result (which we can extract the
  469. new unbacked SymInts out from).
  470. """
  471. # Inputs never need rebinding
  472. if n.op == "placeholder":
  473. return
  474. if bindings := resolve_unbacked_bindings(
  475. shape_env, n.meta.get("unbacked_bindings")
  476. ):
  477. if shape_env is None:
  478. raise AssertionError("shape_env should not be None")
  479. for raw_u0, path in bindings.items():
  480. u1 = pytree.key_get(result, path)
  481. # Sometimes, things were previously unbacked bindings become constants.
  482. # There are two situations this can happen.
  483. #
  484. # First, you might have a runtime assert that causes the
  485. # constant-ification. In this case, the /binding/ itself will
  486. # still be an unbacked symbol (because we will only force it
  487. # to be a constant later in fake tensor propagation). In this
  488. # case, u1 is a SymInt and we still do all our work as normal.
  489. #
  490. # But second, it might be that fake tensor propagation DIRECTLY
  491. # converted the unbacked SymInt into a constant. This happens
  492. # more rarely, but we have identified two situations it can
  493. # validly occur:
  494. #
  495. # - If you have a tensor_version operator, these are initially
  496. # allocated as unbacked SymInts, but after AOTAutograd they
  497. # get forced specialized to specific values. In this case,
  498. # there is no reason to do runtime asserts on them, this is
  499. # just a hack to properly keep track of them to start.
  500. #
  501. # - If you have an item() call on a constant tensor, the result
  502. # of the item() call is constant and we do not need runtime
  503. # asserts on this symbol. In
  504. # https://github.com/pytorch/pytorch/issues/140625 we have a
  505. # case where in the initial trace of the program we are unable
  506. # to determine that torch.tensor is constant, but then
  507. # subsequent passes cause torch.tensor to become a constant and
  508. # then the unbacked symbol goes poof.
  509. #
  510. # In all of these cases, it is no longer necessary to generate
  511. # deferred runtime asserts, since other subsystems (e.g., the
  512. # constant-ification pass) ensure that the quantity is now truly
  513. # static and cannot change at runtime. So it's OK to discard
  514. # in these situations.
  515. #
  516. # There is one more hazard (re
  517. # https://github.com/pytorch/pytorch/issues/141248), the problem
  518. # is that you can end up with "dangling" unbacked symbols that
  519. # exist in the ShapeEnv but are never bound anywhere. You might
  520. # like an invariant that unbacked symbols never get lost. But
  521. # we do not have this invariant, so do not try to enforce it.
  522. if isinstance(u1, (int, float)):
  523. log.info(
  524. "rebind_unbacked: discard %s %s %s -> %s",
  525. n.target,
  526. raw_u0,
  527. path,
  528. u1,
  529. )
  530. continue
  531. # We only care about rebinding unbacked things
  532. if u1.node.hint is not None:
  533. continue
  534. # unbacked symbols bindings might be replaced to other backed or
  535. # unbacked replacements.
  536. #
  537. # Example:
  538. # u = x.item()
  539. # torch._check(u == 5)
  540. #
  541. # The safest approach is to retrieve raw_u1 from u1.node._expr
  542. # and perform the rebinding on the original unbacked symbol,
  543. # even if it’s no longer directly referenced.
  544. #
  545. # In other words, we should always rebind the original symbol
  546. # before any replacements are applied.
  547. # u0 -> u0 == s1
  548. raw_u1 = u1.node._expr
  549. # TODO Do we still need this logic below?
  550. # Simplify SymBool binding
  551. if (
  552. isinstance(raw_u1, sympy.Piecewise)
  553. and len(raw_u1.args) == 2
  554. and (
  555. raw_u1_args0 := cast(
  556. tuple[sympy.Basic, sympy.Basic], raw_u1.args[0]
  557. )
  558. )
  559. and raw_u1_args0[0] == 1
  560. and isinstance(eq := raw_u1_args0[1], sympy.Eq)
  561. and isinstance(new_raw_u1 := eq.lhs, sympy.Symbol)
  562. and shape_env.var_to_range[new_raw_u1].issubset(ValueRanges(0, 1))
  563. and eq.rhs == 1
  564. and cast(tuple[sympy.Basic, sympy.Basic], raw_u1.args[1]) == (0, True)
  565. ):
  566. # This is what the pattern match above is testing
  567. repacked = _sympy_cast_symbool_to_symint_guardless(
  568. sympy.Eq(new_raw_u1, 1)
  569. )
  570. if repacked != raw_u1:
  571. raise AssertionError(f"{repacked} != {raw_u1}")
  572. # Cancel the to_int(to_bool(x)). This is sound because x in
  573. # [0, 1]
  574. raw_u1 = new_raw_u1
  575. if not isinstance(raw_u1, sympy.Symbol):
  576. if raw_u1.free_symbols:
  577. raise AssertionError(f"should have been constant, but got {raw_u1}")
  578. continue
  579. # The old and new could be the same if you improperly hit the memo
  580. # while retracing. Make sure you updated FakeTensorMode.epoch
  581. if raw_u0 == raw_u1:
  582. raise AssertionError(f"{raw_u0} possible memo disaster")
  583. # Reuse the OLD symbol name
  584. shape_env._rename_unbacked_to(raw_u1, raw_u0)
  585. # NB: You could try to expand this to cover more cases by simply
  586. # detecting whenever you have an int output, but this is a bit
  587. # dangerous in case someone adds a function that returns an int but is
  588. # mutating. So manually whitelist for now.
  589. def is_accessor_node(node: torch.fx.Node) -> bool:
  590. """
  591. Helper function to determine if a node is trying to access
  592. a symbolic integer such as size, stride, offset or item. Currently
  593. primarily only used in a DCE pass to figure out purity.
  594. """
  595. # Dynamo only exercised condition
  596. if (
  597. node.op == "call_method"
  598. and isinstance(node.args[0], torch.fx.Node)
  599. and isinstance(node.args[0].meta.get("example_value"), torch.Tensor)
  600. and node.target in ["size", "stride", "storage_offset", "item"]
  601. ):
  602. return True
  603. if node.op == "call_function" and node.target in [
  604. torch.ops.aten.sym_size,
  605. torch.ops.aten.sym_size.default,
  606. torch.ops.aten.sym_size.int,
  607. torch.ops.aten.sym_stride,
  608. torch.ops.aten.sym_stride.default,
  609. torch.ops.aten.sym_stride.int,
  610. torch.ops.aten.sym_storage_offset,
  611. torch.ops.aten.sym_storage_offset.default,
  612. torch.ops.aten.sym_numel.default,
  613. ]:
  614. return True
  615. return False
  616. def canonicalize_bool_expr(expr: _T) -> _T:
  617. """
  618. Canonicalize a boolean expression by transforming it into a lt / le
  619. inequality and moving all the non-constant terms to the rhs.
  620. We canonicalize And / Ors / Not via cnf and then canonicalize their subexpr
  621. recursively
  622. nb. sympy.Rel.canonical is not good enough https://github.com/sympy/sympy/issues/25924
  623. Args:
  624. expr (sympy.Expr): Expression to canonicalize
  625. """
  626. # Canonicalise an inequality by transforming it into a lt / le
  627. # inequality and moving all the non-constant terms to the rhs
  628. # We canonicalise And / Ors / Not via cnf
  629. # nb. Relational.canonical in sympy is broken
  630. # https://github.com/sympy/sympy/issues/25924
  631. if not isinstance(
  632. expr, (sympy.Rel, sympy.And, sympy.Or, sympy.Not, sympy.Eq, sympy.Ne)
  633. ):
  634. return expr
  635. if isinstance(expr, (sympy.And, sympy.Or, sympy.Not)):
  636. expr = sympy.logic.boolalg.to_cnf(expr)
  637. return _canonicalize_bool_expr_impl(expr) # type: ignore[arg-type, return-value]
  638. def _sympy_from_args(
  639. cls: type[Union[sympy.Add, sympy.Mul]],
  640. args: list[sympy.Expr],
  641. sort: bool = True,
  642. is_commutative: Optional[bool] = None,
  643. ) -> sympy.Expr:
  644. """
  645. Create a sympy expression from a list of arguments, optimizing for performance.
  646. This function creates a sympy Add or Mul expression from a list of arguments
  647. while avoiding expensive operations like flattening. It handles sorting the
  648. arguments appropriately based on the expression type.
  649. Args:
  650. cls: The sympy class to create (Add or Mul)
  651. args: List of sympy expressions to combine
  652. sort: Whether to sort the arguments (default: True)
  653. is_commutative: Whether the operation is commutative (default: None)
  654. Returns:
  655. A sympy expression of type cls combining all arguments
  656. Raises:
  657. ValueError: If cls is not sympy.Add or sympy.Mul
  658. """
  659. if not args:
  660. return cls.identity # type: ignore[union-attr]
  661. # These args are already in canonical form, so we avoid calling
  662. # Add(*args) to avoid expensive Add.flatten operation
  663. if sort:
  664. if cls is sympy.Add:
  665. sort_fn = sympy.core.add._addsort
  666. elif cls is sympy.Mul:
  667. sort_fn = sympy.core.mul._mulsort
  668. else:
  669. raise ValueError(f"Unknown cls: {cls}")
  670. # we don't support non commutative with sort
  671. if is_commutative is not True:
  672. raise AssertionError("is_commutative must be True")
  673. if args[0].is_Number:
  674. rest = args[1:]
  675. sort_fn(rest)
  676. return cls._from_args([args[0]] + rest, is_commutative=is_commutative) # type: ignore[attr-defined]
  677. else:
  678. args = args.copy()
  679. sort_fn(args)
  680. return cls._from_args(args, is_commutative=is_commutative) # type: ignore[attr-defined]
  681. else:
  682. # if the args are already sorted, we create directly
  683. return cls._from_args(args, is_commutative=is_commutative) # type: ignore[attr-defined]
  684. def _canonicalize_bool_expr_impl(expr: SympyBoolean) -> SympyBoolean:
  685. """
  686. After canonicalization, we are guaranteed to have eliminated Ge/Gt relations
  687. (rewriting them to Le/Lt, respectively).
  688. """
  689. if isinstance(expr, (sympy.And, sympy.Or)):
  690. return type(expr)(*map(canonicalize_bool_expr, expr.args))
  691. opposite = {sympy.Gt: sympy.Lt, sympy.Ge: sympy.Le}
  692. t: Union[type[Any]]
  693. if isinstance(expr, tuple(opposite.keys())):
  694. rhs = expr.lhs - expr.rhs # type: ignore[attr-defined]
  695. t = opposite[type(expr)] # type: ignore[index]
  696. else:
  697. if not isinstance(expr, (sympy.Lt, sympy.Le, sympy.Eq, sympy.Ne)):
  698. raise AssertionError(f"Expected Lt/Le/Eq/Ne, got {type(expr)}")
  699. rhs = expr.rhs - expr.lhs
  700. t = type(expr)
  701. def is_neg(t: sympy.Expr) -> bool:
  702. return (t.is_Number and t.is_negative) or (
  703. isinstance(t, sympy.Mul) and t.args[0].is_Number and t.args[0].is_negative
  704. )
  705. lhs = S.Zero
  706. rhs = _reduce_to_lowest_terms(rhs)
  707. if isinstance(rhs, sympy.Add):
  708. pos = []
  709. neg = []
  710. for term in rhs.args:
  711. if is_neg(term):
  712. neg.append(-term)
  713. else:
  714. pos.append(term)
  715. # these are already sorted
  716. rhs = _sympy_from_args(sympy.Add, pos, sort=False, is_commutative=True)
  717. # the terms were changed, so needs a sorting
  718. lhs = _sympy_from_args(sympy.Add, neg, sort=True, is_commutative=True)
  719. elif is_neg(rhs):
  720. # lhs == 0
  721. lhs, rhs = -rhs, S.Zero
  722. # We don't have to evaluate here because lhs, rhs came from a Boolean
  723. # and it was already simplified
  724. return t(lhs, rhs, evaluate=False)
  725. def _reduce_to_lowest_terms(expr: sympy.Expr) -> sympy.Expr:
  726. """
  727. Eliminates any integer factor from a given expression.
  728. E.g., 6x + 4y reduces to 3x + 2y.
  729. Useful when an expression is == or != to 0.
  730. """
  731. def integer_coefficient(x: sympy.Expr) -> int:
  732. if x.is_Integer:
  733. return abs(int(x))
  734. elif x.is_Mul:
  735. # If one of the args of a Mul is an Integer, it is the
  736. # first arg. eg: args(2*x*3*y) == (6, x, y)
  737. return abs(int(x.args[0])) if x.args[0].is_Integer else 1 # type: ignore[call-overload]
  738. else:
  739. return 1
  740. def div_by_factor(x: sympy.Expr, factor: int) -> sympy.Expr:
  741. if x.is_Integer:
  742. return x / factor
  743. elif x.is_Mul:
  744. if x.args[0] != factor:
  745. args = [x.args[0] / sympy.Integer(factor), *x.args[1:]]
  746. else:
  747. # Mul._from_args require a canonical list of args
  748. # so we remove the first arg (x.args[0] / factor) if it was 1
  749. args = list(x.args[1:])
  750. return _sympy_from_args(sympy.Mul, args, is_commutative=x.is_commutative)
  751. else:
  752. raise AssertionError(f"illegal arg to div_by_factor: {x}")
  753. if expr.is_Add:
  754. atoms = cast(Sequence[sympy.Expr], expr.args)
  755. factor = functools.reduce(math.gcd, map(integer_coefficient, atoms))
  756. if factor == 1:
  757. return expr
  758. # pyrefly: ignore [bad-argument-type]
  759. atoms = [div_by_factor(x, factor) for x in atoms]
  760. return _sympy_from_args(
  761. sympy.Add, atoms, sort=True, is_commutative=expr.is_commutative
  762. )
  763. elif expr.is_Integer:
  764. return S.One
  765. elif expr.is_Mul:
  766. return div_by_factor(expr, integer_coefficient(expr))
  767. return expr
  768. def is_nested_int(s: IntLikeType) -> TypeGuard[SymInt]:
  769. return isinstance(s, torch.SymInt) and s.node.is_nested_int()
  770. IterateExprsAtom: TypeAlias = Union[
  771. SymInt, SymFloat, SymBool, int, float, bool, sympy.Basic, torch.Tensor
  772. ]
  773. IterateExprs: TypeAlias = Union[IterateExprsAtom, Sequence[IterateExprsAtom]]
  774. def _iterate_exprs(val: IterateExprs) -> Iterator[sympy.Basic]:
  775. """
  776. Recursively iterate through a value and yield all sympy expressions contained within it.
  777. This function traverses various data structures (tensors, lists, tuples, etc.) and extracts
  778. any symbolic expressions they contain. It's used for operations like finding free symbols
  779. in complex nested structures.
  780. Args:
  781. val: The value to extract sympy expressions from. Can be a symbolic type (SymInt, SymFloat, SymBool),
  782. a sympy expression, a primitive type (int, float, bool), a container (tuple, list),
  783. a sparse tensor, a regular tensor, None, or a torch.Generator.
  784. Yields:
  785. sympy.Basic: Each sympy expression found in the value.
  786. Raises:
  787. AssertionError: If the value is of an unsupported type.
  788. """
  789. # This is almost close enough to implement in terms of _iterate_nodes()
  790. # except that it needs to handle `list[sympy.Basic]` which _iterate_nodes()
  791. # can't handle.
  792. if isinstance(val, SymTypes):
  793. # This allow applies to the jagged layout NestedTensor case as
  794. # nested ints are not symbolic
  795. if is_symbolic(val):
  796. yield val.node.expr
  797. elif isinstance(val, SymNode):
  798. yield val.expr
  799. elif isinstance(val, sympy.Basic):
  800. yield val
  801. elif isinstance(val, (int, float, bool)):
  802. pass
  803. elif isinstance(val, (tuple, list)):
  804. for s in val:
  805. yield from _iterate_exprs(s)
  806. elif is_sparse_any(val):
  807. yield from _iterate_exprs(val.size())
  808. elif isinstance(val, torch.Tensor):
  809. yield from _iterate_exprs(val.size())
  810. yield from _iterate_exprs(val.stride())
  811. yield from _iterate_exprs(val.storage_offset())
  812. elif val is None:
  813. pass
  814. # see Note: [Generator arguments in AOTDispatcher]
  815. elif isinstance(val, torch.Generator) or is_opaque_value(val):
  816. pass
  817. elif isinstance(val, FakeScriptObject):
  818. pass
  819. else:
  820. raise AssertionError(f"cannot extract sympy expressions from {val} {type(val)}")
  821. def _iterate_nodes(val: Any) -> Iterator[SymNode]:
  822. """
  823. Recursively iterate through a value and yield all SymNodes contained
  824. within it.
  825. """
  826. if isinstance(val, SymNode):
  827. yield val
  828. elif isinstance(val, py_sym_types):
  829. # This allow applies to the jagged layout NestedTensor case as
  830. # nested ints are not symbolic
  831. if is_symbolic(val):
  832. yield val.node
  833. elif isinstance(val, (tuple, list, torch.Size)):
  834. for s in val:
  835. yield from _iterate_nodes(s)
  836. elif isinstance(val, torch.Tensor):
  837. yield from _iterate_nodes(val.size())
  838. if not is_sparse_any(val):
  839. yield from _iterate_nodes(val.stride())
  840. yield from _iterate_nodes(val.storage_offset())
  841. def free_symbols(val: IterateExprs) -> OrderedSet[sympy.Symbol]:
  842. """
  843. Recursively collect all free symbols from a value.
  844. This function traverses various data structures (tensors, lists, tuples, etc.) and extracts
  845. all sympy symbols contained within them. It's useful for finding all symbolic variables
  846. that a complex nested structure depends on.
  847. Args:
  848. val: The value to extract symbols from. Can be a symbolic type (SymInt, SymFloat, SymBool),
  849. a container (tuple, list), a tensor, or None.
  850. Returns:
  851. OrderedSet[sympy.Symbol]: An ordered set of all free symbols found in the value.
  852. """
  853. if val is None:
  854. return OrderedSet()
  855. itr = _iterate_exprs(val)
  856. # we need at least 1 to call union, so we hand code the identity
  857. try:
  858. first_expr = next(itr)
  859. except StopIteration:
  860. return OrderedSet()
  861. # TODO: Apparently, returning an OrderedSet here breaks
  862. # python test/distributed/tensor/test_dtensor_compile.py TestDTensorCompile.test_dtensor_dynamic
  863. return first_expr.free_symbols.union(*(e.free_symbols for e in itr)) # type: ignore[return-value]
  864. def has_free_symbols(val: IterateExprs) -> bool:
  865. """Faster version of bool(free_symbols(val))"""
  866. return not all((e.is_number or e.is_Boolean) for e in _iterate_exprs(val))
  867. def has_free_unbacked_symbols(x: IterateExprs) -> bool:
  868. """Faster version of bool(free_unbacked_symbols(val))"""
  869. from sympy.core.traversal import iterargs
  870. for s in _iterate_exprs(x):
  871. for arg in iterargs(s):
  872. if arg.is_Symbol and symbol_is_type(
  873. arg, (SymT.UNBACKED_INT, SymT.UNBACKED_FLOAT)
  874. ):
  875. return True
  876. return False
  877. def free_unbacked_symbols(x: IterateExprs) -> OrderedSet[sympy.Symbol]:
  878. """Like free_symbols, but filtered to only report unbacked symbols"""
  879. # NB: keep synced with is_unbacked_symint
  880. return OrderedSet(
  881. s
  882. for s in free_symbols(x)
  883. if symbol_is_type(s, (SymT.UNBACKED_INT, SymT.UNBACKED_FLOAT))
  884. )
  885. def _free_non_source_unbacked_symbols(
  886. x: IterateExprs, unbacked_inputs: OrderedSet[sympy.Symbol]
  887. ) -> OrderedSet[sympy.Symbol]:
  888. """Unbacked symbols that are not inputs to the graph. These are symbols that originated from
  889. data-dependent operations as opposed to mark_unbacked calls."""
  890. unbacked_symbols = free_unbacked_symbols(x)
  891. non_source_symbols = unbacked_symbols - unbacked_inputs
  892. return non_source_symbols
  893. # WARNING: Don't use this on Dynamo produced graphs, they don't have meta
  894. # setup!
  895. def is_symbol_binding_fx_node(node: torch.fx.Node) -> Optional[sympy.Symbol]:
  896. """
  897. Check if a given FX node is a symbol binding node.
  898. A symbol binding node is one that has a SymInt value in its meta that contains
  899. a sympy Symbol expression, and is either a placeholder node or contains unbacked symbols.
  900. Args:
  901. node (torch.fx.Node): The FX node to check
  902. Returns:
  903. Optional[sympy.Symbol]: The sympy Symbol if the node is a symbol binding node, None otherwise
  904. """
  905. if (
  906. "val" in node.meta
  907. and isinstance(node.meta["val"], torch.SymInt)
  908. and isinstance(node.meta["val"].node.expr, sympy.Symbol)
  909. and (
  910. node.op == "placeholder"
  911. or free_unbacked_symbols(node.meta["val"].node.expr)
  912. )
  913. ):
  914. return node.meta["val"].node.expr
  915. return None
  916. def find_symbol_binding_fx_nodes(
  917. graph: torch.fx.Graph,
  918. ) -> dict[sympy.Symbol, torch.fx.Node]:
  919. """
  920. Find all nodes in an FX graph that bind sympy Symbols.
  921. This function scans through all nodes in the given FX graph and identifies
  922. nodes that bind sympy Symbols (typically placeholder nodes with SymInt values).
  923. When multiple nodes bind the same symbol, only the first occurrence is kept.
  924. Args:
  925. graph: The FX graph to search for symbol binding nodes
  926. Returns:
  927. A dictionary mapping from sympy Symbols to their binding FX nodes
  928. """
  929. r = {}
  930. # NB: Prefer first occurrence of symbol
  931. for node in graph.nodes:
  932. if (s := is_symbol_binding_fx_node(node)) is not None and s not in r:
  933. r[s] = node
  934. return r
  935. @dataclass(frozen=True, slots=True)
  936. class Specialization:
  937. """
  938. This class is used in multi-graph compilation contexts where we generate
  939. multiple specialized graphs and dispatch to the appropriate one at runtime.
  940. This allows us to optimize the trade-off between performance and generality
  941. by creating specialized versions for common patterns (e.g., x.shape[0] % 16 == 0)
  942. while maintaining a general fallback.
  943. """
  944. source: TensorPropertySource
  945. check_fn: Callable
  946. # Analogous to ConvertIntSource
  947. @dataclass(frozen=True, slots=True)
  948. class ConvertIntKey:
  949. def __str__(self) -> str:
  950. return ".cast_symbool_to_symint_guardless()"
  951. def get(self, b: bool) -> IntLikeType:
  952. """Get the int value from bool"""
  953. return cast_symbool_to_symint_guardless(b)
  954. @dataclass(frozen=True, slots=True)
  955. class CallMethodKey:
  956. name: str
  957. def __str__(self) -> str:
  958. return f".{self.name}()"
  959. def get(self, o: Any) -> Any:
  960. """Call the method on object"""
  961. return getattr(o, self.name)()
  962. @dataclass(frozen=True, slots=True)
  963. class InnerTensorKey:
  964. inner_name: str
  965. def __str__(self) -> str:
  966. return f".{self.inner_name}"
  967. def get(self, o: Any) -> Any:
  968. """Get the inner tensor attribute"""
  969. return getattr(o, self.inner_name)
  970. @dataclass(frozen=True, slots=True)
  971. class DivideByKey:
  972. divisor: IntLikeType
  973. def __str__(self) -> str:
  974. return f".__floordiv__({self.divisor})"
  975. def get(self, o: int) -> int:
  976. """Divide object by divisor"""
  977. return o // self.divisor
  978. def _free_unbacked_symbols_with_path(
  979. a: object,
  980. path: pytree.KeyPath,
  981. real: Optional[object] = None,
  982. shape_env: Optional[ShapeEnv] = None,
  983. pending: Optional[set[sympy.Symbol]] = None,
  984. simplify: bool = False,
  985. ) -> dict[sympy.Symbol, pytree.KeyPath]:
  986. """
  987. Recursively traverses a structure to find unbacked symbols and their access paths.
  988. This function walks through tensors, lists, tuples, and symbolic values to locate
  989. unbacked symbols that are in the pending set, and returns a mapping from those
  990. symbols to their access paths in the structure.
  991. Args:
  992. a: The object to traverse (tensor, list, tuple, SymInt, etc.)
  993. path: The current path in the object tree
  994. real: Optional real tensor corresponding to the fake tensor being traversed
  995. shape_env: Optional ShapeEnv to register unbacked values with
  996. pending: Set of unbacked symbols to look for (will be modified in-place)
  997. simplify: Whether to use simplified expressions
  998. Returns:
  999. A dictionary mapping unbacked symbols to their access paths
  1000. """
  1001. go = functools.partial(
  1002. _free_unbacked_symbols_with_path,
  1003. shape_env=shape_env,
  1004. pending=pending,
  1005. simplify=simplify,
  1006. )
  1007. def expr(s: Union[SymInt, SymFloat, SymBool]) -> sympy.Expr:
  1008. if simplify:
  1009. return s.node.expr
  1010. # (When called from compute_unbacked_bindings)
  1011. # NB: Intentionally access _expr, not expr, do not want
  1012. # simplification!
  1013. return s.node._expr
  1014. if pending is None:
  1015. pending = set()
  1016. r = {}
  1017. def match_tensor(a: torch.Tensor, real_tensor: Optional[torch.Tensor] = None):
  1018. r.update(
  1019. go(
  1020. a.size(),
  1021. path + (CallMethodKey("size"),),
  1022. real=real_tensor.size() if real_tensor is not None else None,
  1023. )
  1024. )
  1025. if a.layout not in [
  1026. torch.sparse_csr,
  1027. torch.sparse_csc,
  1028. torch.sparse_bsr,
  1029. torch.sparse_bsc,
  1030. ]:
  1031. r.update(
  1032. go(
  1033. a.stride(),
  1034. path + (CallMethodKey("stride"),),
  1035. real=real_tensor.stride() if real_tensor is not None else None,
  1036. )
  1037. )
  1038. r.update(
  1039. go(
  1040. a.storage_offset(),
  1041. path + (CallMethodKey("storage_offset"),),
  1042. real=(
  1043. real_tensor.storage_offset() if real_tensor is not None else None
  1044. ),
  1045. )
  1046. )
  1047. if isinstance(a, (tuple, list)):
  1048. # NB: real is apparently not always a tuple/list here
  1049. # python test/inductor/test_torchinductor.py CpuTests.test_index_propagation_nested_indirect_indexing_cpu
  1050. for i in range(len(a)):
  1051. r.update(
  1052. go(
  1053. a[i],
  1054. path + (pytree.SequenceKey(i),),
  1055. real=real[i] if real is not None else None, # type: ignore[index]
  1056. )
  1057. )
  1058. elif is_traceable_wrapper_subclass(a):
  1059. # TODO: Determine if this is correct
  1060. attrs, _ = a.__tensor_flatten__()
  1061. for attr in attrs:
  1062. sub = getattr(a, attr)
  1063. r.update(go(sub, path + (InnerTensorKey(attr),)))
  1064. # match DTensor outer shapes
  1065. if torch.distributed.is_available() and isinstance(
  1066. a, torch.distributed.tensor.DTensor
  1067. ):
  1068. match_tensor(a)
  1069. elif isinstance(a, torch.Tensor) and is_batchedtensor(a):
  1070. unwrapped_tensor = get_unwrapped(a)
  1071. r.update(go(unwrapped_tensor, path))
  1072. elif isinstance(a, torch.Tensor) and not is_batchedtensor(a):
  1073. from torch._subclasses.fake_tensor import FakeTensor
  1074. if not isinstance(a, FakeTensor):
  1075. raise AssertionError(f"Expected FakeTensor, got {type(a)}")
  1076. match_tensor(a, a.real_tensor)
  1077. elif (
  1078. isinstance(a, (torch.SymInt, torch.SymFloat))
  1079. and isinstance(s := expr(a), sympy.Symbol)
  1080. and s in pending
  1081. ):
  1082. r[s] = path
  1083. if shape_env and real is not None:
  1084. if not isinstance(real, (int, float)):
  1085. raise AssertionError(f"Expected int or float, got {type(real)}")
  1086. shape_env.set_real_tensor_prop_unbacked_vals(s, real)
  1087. pending.remove(s)
  1088. # When an unbacked SymInt is perfectly divisible by an integer
  1089. # constant, we replace it with the integer constant to improve
  1090. # reasoning capabilities. However, in synthetic examples, it is
  1091. # then possible that the factor never is explicitly allocated.
  1092. # Fortunately, we can compute it by division.
  1093. elif (
  1094. isinstance(a, torch.SymInt)
  1095. and isinstance(s := expr(a), sympy.Mul)
  1096. and len(s.args) == 2
  1097. and isinstance(lhs := s.args[0], (sympy.Integer, sympy.Symbol))
  1098. and isinstance(rhs := s.args[1], sympy.Symbol)
  1099. # support exactly one unbacked for now
  1100. and ((rhs in pending) ^ (lhs in pending))
  1101. # support constant coefficient or backed symbolic coefficient
  1102. and (
  1103. isinstance(coeff := lhs if lhs not in pending else rhs, sympy.Integer)
  1104. or shape_env
  1105. and coeff in shape_env.backed_var_to_val
  1106. )
  1107. ):
  1108. def _symint_wrap(s: sympy.Symbol) -> SymInt:
  1109. return shape_env.create_symintnode( # type: ignore[union-attr]
  1110. s,
  1111. hint=int(shape_env.backed_var_to_val[s]), # type: ignore[union-attr]
  1112. source=shape_env.var_to_sources.get(s, [None])[0], # type: ignore[union-attr]
  1113. )
  1114. unbacked = lhs if lhs in pending else rhs
  1115. divisor: IntLikeType = (
  1116. int(coeff)
  1117. if shape_env and isinstance(coeff, sympy.Integer)
  1118. else _symint_wrap(coeff)
  1119. )
  1120. # TODO: DivideByKey needs to test divisibility at runtime!
  1121. # pyrefly: ignore [unsupported-operation]
  1122. r[unbacked] = path + (DivideByKey(divisor),)
  1123. if real is not None:
  1124. if not isinstance(real, int):
  1125. raise AssertionError(f"Expected int, got {type(real)}")
  1126. val = (
  1127. real // int(coeff)
  1128. if isinstance(coeff, sympy.Integer)
  1129. else CleanDiv(real, coeff)
  1130. )
  1131. if shape_env:
  1132. shape_env.set_real_tensor_prop_unbacked_vals(unbacked, val)
  1133. pending.remove(unbacked)
  1134. # The annoyance here arises from the fact that SymBool is
  1135. # allocated by allocating a SymInt and then testing if it's equal
  1136. # to one. So you have a complicated binding site logic for this.
  1137. elif (
  1138. isinstance(a, torch.SymBool)
  1139. and isinstance(s := expr(a), sympy.Eq)
  1140. # This must match create_unbacked_symbool EXACTLY
  1141. and isinstance(s.lhs, sympy.Symbol)
  1142. and s.rhs == 1
  1143. and s.lhs in pending
  1144. ):
  1145. # pyrefly: ignore [unsupported-operation]
  1146. r[s.lhs] = path + (ConvertIntKey(),)
  1147. if real is not None:
  1148. if type(real) is not bool:
  1149. raise AssertionError(f"Expected bool, got {type(real)}")
  1150. if shape_env:
  1151. shape_env.set_real_tensor_prop_unbacked_vals(s, int(real))
  1152. pending.remove(s.lhs)
  1153. return r
  1154. def compute_unbacked_bindings(
  1155. shape_env: Optional[ShapeEnv],
  1156. example_value: object,
  1157. old_example_value: Optional[object] = None,
  1158. peek: bool = False,
  1159. ) -> Optional[dict[sympy.Symbol, pytree.KeyPath]]:
  1160. """
  1161. After having run fake tensor propagation and producing example_value
  1162. result, traverse example_value looking for freshly bound unbacked
  1163. symbols and record their paths for later. It is an error if
  1164. we have allocated an unbacked SymInt but it cannot be found in
  1165. example_value. (NB: this means if you have a multi-output
  1166. function, you must call this on the tuple of tensor output, you
  1167. cannot wait!)
  1168. The peek parameter lets you check out what the bindings are without
  1169. changing the affected list. This is primarily useful for ensuring
  1170. real_tensor_prop_unbacked_vals is promptly populated when propagate_real_tensors is on.
  1171. """
  1172. if shape_env is None:
  1173. return None
  1174. fresh_sym = shape_env.pending_fresh_unbacked_symbols
  1175. ign_sym = shape_env.ignorable_fresh_unbacked_symbols
  1176. pending = set(fresh_sym)
  1177. ignorable = set(ign_sym)
  1178. if not peek:
  1179. if pending:
  1180. log.info("compute_unbacked_bindings %s", fresh_sym)
  1181. fresh_sym.clear()
  1182. ign_sym.clear()
  1183. if not pending:
  1184. return None
  1185. symbol_to_path = _free_unbacked_symbols_with_path(
  1186. example_value, (), shape_env=shape_env, pending=pending, simplify=False
  1187. )
  1188. pending -= ignorable
  1189. if not peek and pending:
  1190. extra = (
  1191. repr((example_value.stride(), example_value.storage_offset()))
  1192. if isinstance(example_value, torch.Tensor)
  1193. else ""
  1194. )
  1195. msg = (
  1196. f"Pending unbacked symbols {pending} not in returned outputs {example_value} {extra}.\n"
  1197. "Did you accidentally call new_dynamic_size() or item() more times "
  1198. "than you needed to in your fake implementation?\n"
  1199. "For more help, see https://docs.google.com/document/d/1RWrH-3wLEpzR9kCS6gGBNen_-Fs-8PVbWWFE5AcgeWE/edit"
  1200. )
  1201. if torch.fx.experimental._config.soft_pending_unbacked_not_found_error:
  1202. log.warning(msg)
  1203. else:
  1204. raise PendingUnbackedSymbolNotFound(msg)
  1205. # Why do we have to do some rebinding here? If the original FX node
  1206. # wasn't a binding site because you had a memo hit, but post
  1207. # translation you aren't a memo hit anymore, there's now a new binding
  1208. # site... but we know (because it's the same FX node) that the value
  1209. # is actually the same, they're just not obviously equal anymore.
  1210. #
  1211. # The logic here is written carefully, because unlike the
  1212. # bind_unbacked case, we are not guaranteed to have a symbol for
  1213. # old_sym. If we have a symbol, do regular rename unbacked to; but if
  1214. # we don't, we need to specially eliminate the fresh unbacked symbol
  1215. # (NB: we are /trusting/ that the memoization is correct, and that we
  1216. # don't need to generate a new runtime assert. This is load bearing,
  1217. # as repropagation can happen after we've frozen runtime asserts.)
  1218. if old_example_value is not None:
  1219. for keypath in symbol_to_path.values():
  1220. old_sym = pytree.key_get(old_example_value, keypath)
  1221. new_sym = pytree.key_get(example_value, keypath)
  1222. if isinstance(new_sym, SymTypes) and isinstance(
  1223. new_s := new_sym.node.expr, sympy.Symbol
  1224. ):
  1225. if (
  1226. isinstance(old_sym, SymTypes)
  1227. and (old_s := old_sym.node.expr) != new_s
  1228. ):
  1229. # If old_s is not an unbacked_symbol,
  1230. # we assume that the original unbacked symbol is replaced
  1231. # by a backed symbol (old_s). This can happen
  1232. # when this node reuses the original symbol (due to memoi)
  1233. # and the original symbol gets replaced by the backed symbol.
  1234. # When this happens we just replace new_s by the old_s
  1235. # because we know the value is the same.
  1236. if isinstance(old_s, sympy.Symbol) and free_unbacked_symbols(old_s):
  1237. shape_env._rename_unbacked_to(new_s, old_s)
  1238. else:
  1239. shape_env._eliminate_unbacked(new_s, old_s)
  1240. elif not isinstance(old_sym, SymTypes):
  1241. shape_env._eliminate_unbacked(new_s, sympy.sympify(old_sym))
  1242. return symbol_to_path
  1243. # Note [guard_or_]
  1244. # The following two functions are common utilities used while defining unbacked semantics
  1245. # of various framework code. Those would be used in situations you prefer to guard and know
  1246. # the result of the expression over not guarding, but in case you hit a data dependent error
  1247. # you are ok with just returning true or false.
  1248. #
  1249. # When to use this?
  1250. # (1) If you can use a higher level combinator prefer using those instead, they are definitely safe (modulo short-circuiting).
  1251. #
  1252. # (2) It can be used if the program would behave equivalently if _guard_or returned true or false.
  1253. # Many inductor optimizations fall in this bracket for example.
  1254. #
  1255. # (3) Finally, it's even be OK if the program wouldn't behave equivalently, so long as the
  1256. # change is semantics preserving. It can be semantics preserving if the program errors in more
  1257. # cases than it did previously (but otherwise behaves identically), or if it changes some quantity
  1258. # in a way that doesn't matter (e.g., strides often fall in this bucket.)
  1259. #
  1260. # (4) Specialize for the general case and add a runtime assertion that would fail during
  1261. # runtime if the conditions for the general case are not satisfied. Examples for this are;
  1262. # assuming expand/reshape inputs are not -1. or assuming the non-broadcasting path.
  1263. #
  1264. def _guard_or(a: BoolLikeType, default: bool) -> bool:
  1265. """
  1266. Try to guard a, if data dependent error encountered just return default.
  1267. """
  1268. if not isinstance(a, SymBool):
  1269. if not isinstance(a, bool):
  1270. raise AssertionError(f"Expected bool, got {type(a)}")
  1271. return a
  1272. # if backed_size_oblivious is True we treat backed as unbacked here.
  1273. if torch.fx.experimental._config.backed_size_oblivious:
  1274. result = _static_eval_sym_bool(a)
  1275. return result if result is not None else default
  1276. shape_env = getattr(a.node, "shape_env", None)
  1277. # xla symnode path.
  1278. if shape_env is None:
  1279. return guard_bool(a)
  1280. sym_node = a.node
  1281. r = sym_node.shape_env.evaluate_sym_node(
  1282. sym_node, size_oblivious=False, fallback_value=default
  1283. )
  1284. return bool(r)
  1285. def guard_or_false(a: BoolLikeType) -> bool:
  1286. """
  1287. Try to guard a, if data dependent error encountered just return false.
  1288. """
  1289. return _guard_or(a, False)
  1290. def guard_or_true(a: BoolLikeType) -> bool:
  1291. """
  1292. Try to guard a, if data dependent error encountered just return true.
  1293. """
  1294. return _guard_or(a, True)
  1295. def _static_eval_sym_bool(x: SymBool) -> Optional[bool]:
  1296. if not isinstance(x, SymBool):
  1297. raise AssertionError(f"Expected SymBool, got {type(x)}")
  1298. expr = x.node.expr
  1299. try:
  1300. # Shape env access is inside the try on purpose. xla symnode does not
  1301. # have it on its attributes.
  1302. shape_env = x.node.shape_env
  1303. simplified = shape_env._maybe_evaluate_static(expr)
  1304. if simplified is not None:
  1305. return bool(simplified)
  1306. else:
  1307. return None
  1308. except Exception:
  1309. log.debug("Could not simplify %s", expr)
  1310. return None
  1311. def statically_known_false(x: BoolLikeType) -> bool:
  1312. """
  1313. Returns True if x can be simplified to a constant and is False.
  1314. If x cannot be evaluated from static, we return False
  1315. .. note::
  1316. This function doesn't introduce new guards, so the expression may end
  1317. up evaluating to False at runtime even if this function returns False.
  1318. Args:
  1319. x (bool, SymBool): The expression to try statically evaluating
  1320. """
  1321. if not isinstance(x, SymBool):
  1322. if not isinstance(x, bool):
  1323. raise AssertionError(f"Expected bool, got {type(x)}")
  1324. return not x
  1325. result = _static_eval_sym_bool(x)
  1326. if result is None:
  1327. return False
  1328. return not result
  1329. def statically_known_true(x: BoolLikeType) -> bool:
  1330. """
  1331. Returns True if x can be simplified to a constant and is true.
  1332. .. note::
  1333. This function doesn't introduce new guards, so the expression may end
  1334. up evaluating to true at runtime even if this function returns False.
  1335. Args:
  1336. x (bool, SymBool): The expression to try statically evaluating
  1337. """
  1338. if not isinstance(x, SymBool):
  1339. if not isinstance(x, bool):
  1340. raise AssertionError(f"Expected bool, got {type(x)}")
  1341. return x
  1342. result = _static_eval_sym_bool(x)
  1343. if result is None:
  1344. return False
  1345. return result
  1346. def sym_and(x: BoolLikeType, *others: BoolLikeType) -> BoolLikeType:
  1347. """
  1348. and, but for symbolic expressions, without bool casting.
  1349. """
  1350. if len(others) == 0:
  1351. return x
  1352. for y in others:
  1353. x = operator.and_(x, y)
  1354. return x
  1355. def sym_eq(x: _T, y: _T) -> BoolLikeType:
  1356. """
  1357. Like ==, but when run on list/tuple, it will recursively test equality
  1358. and use sym_and to join the results together, without guarding.
  1359. """
  1360. if isinstance(x, (tuple, list)) and isinstance(y, (list, tuple)):
  1361. if len(x) != len(y):
  1362. return False
  1363. return functools.reduce(operator.and_, map(sym_eq, x, y), True)
  1364. elif isinstance(x, (int, torch.SymInt)) and isinstance(y, (int, torch.SymInt)):
  1365. return x == y
  1366. else:
  1367. raise AssertionError(f"unexpected sym_eq between {type(x)} {type(y)}")
  1368. def sym_or(x: BoolLikeType, *others: BoolLikeType) -> BoolLikeType:
  1369. """
  1370. or, but for symbolic expressions, without bool casting.
  1371. """
  1372. if len(others) == 0:
  1373. return x
  1374. for y in others:
  1375. x = operator.or_(x, y)
  1376. return x
  1377. def guard_scalar(
  1378. a: Union[SymBool, SymInt, SymFloat, int, bool, float],
  1379. ) -> Union[bool, int, float]:
  1380. """
  1381. Guard a scalar value, which can be a symbolic or concrete boolean, integer, or float.
  1382. This function dispatches to the appropriate guard function based on the type of the input.
  1383. Args:
  1384. a: A symbolic or concrete scalar value (bool, int, or float)
  1385. Returns:
  1386. The concrete value after guarding
  1387. Raises:
  1388. AssertionError: If the input is not a recognized scalar type
  1389. """
  1390. if isinstance(a, (SymBool, bool)):
  1391. return guard_bool(a)
  1392. elif isinstance(a, (SymInt, int)):
  1393. return guard_int(a)
  1394. elif isinstance(a, (SymFloat, float)):
  1395. return guard_float(a)
  1396. else:
  1397. raise AssertionError(f"unrecognized scalar {a}")
  1398. def _advise_is_size(a: SymInt) -> None:
  1399. """
  1400. Don't use this directly; use torch._check_is_size instead.
  1401. This is a softer version of _constrain_range_for_size (with min=0,
  1402. max=Inf). Instead of forcibly constraining a variable (and erroring if we
  1403. failed to constrain it), it will simply advise us that a size is
  1404. constrained in some way. We will always defer a runtime assert for this
  1405. constraint if we cannot prove it at compile-time, but we we only
  1406. *sometimes* learn useful extra information at compile-time with this
  1407. information. This is in contrast to constrain_range_for_size, where if
  1408. you don't call that on a fresh unbacked symint, chances are we will choke.
  1409. TODO: Make Dynamo handle this appropriately if this is seen in Dynamo-ed
  1410. code. Right now this is only really used in code with AOTAutograd trace
  1411. through, so it is not a big problem that this isn't supported, but in
  1412. principle all of this code should be Dynamo'able too.
  1413. TODO: I didn't support min/max because I didn't have a use case where this
  1414. actually helped. In principle we can support it, it just makes the
  1415. implementation below more complicated.
  1416. """
  1417. # This must always succeed, because the sole allowed caller _check_is_size
  1418. # was responsible for expect_true'ing this
  1419. # This assert triggers expensive sym compute, do not do it until its cheap.
  1420. # assert a >= 0
  1421. # NB: it's important not to constrain range for size for *hinted* SymInts,
  1422. # because it is not only unsound, it will immediately trip our asserts
  1423. # that hints have to be consistent with static analysis! If you somehow
  1424. # have an unbounded SymInt that later constrains to 1, this will be
  1425. # inconsistent with the range
  1426. if (
  1427. isinstance(a, SymInt)
  1428. and isinstance(a.node, SymNode)
  1429. and isinstance(a.node.expr, sympy.Symbol)
  1430. and a.node.shape_env.is_unbacked_symint(a.node.expr)
  1431. ):
  1432. _constrain_range_for_size(a)
  1433. def _advise_is_bounded(a: SymInt, upper_bound: IntLikeType) -> None:
  1434. if (
  1435. isinstance(a, SymInt)
  1436. and isinstance(a.node, SymNode)
  1437. and isinstance(a.node.expr, sympy.Symbol)
  1438. and a.node.shape_env.is_unbacked_symint(a.node.expr)
  1439. and isinstance(upper_bound, int) # TODO: relax
  1440. ):
  1441. a.node.shape_env._constrain_is_bounded(a.node.expr, upper_bound)
  1442. def _constrain_range_for_size(
  1443. a: SymInt, min: Optional[int] = None, max: Optional[int] = None
  1444. ) -> None:
  1445. """
  1446. This function is NOT INTENDED to be used by itself.
  1447. """
  1448. if isinstance(a, (SymFloat, SymBool)):
  1449. raise ValueError("Constraining SymFloat/SymBool is nyi")
  1450. if not isinstance(a, SymInt):
  1451. raise AssertionError("can only constrain range for SymInt")
  1452. if not isinstance(a.node.expr, sympy.Symbol):
  1453. raise AssertionError(f"constraining non-Symbols NYI: {a}")
  1454. a.node.shape_env._constrain_range_for_size(a.node.expr, min, max)
  1455. # inclusive both ways
  1456. def constrain_range(
  1457. a: SymInt, *, min: Optional[int], max: Optional[int] = None
  1458. ) -> None:
  1459. """
  1460. Applies a constraint that the passed in SymInt must lie between min-max
  1461. inclusive-inclusive, WITHOUT introducing a guard on the SymInt (meaning
  1462. that it can be used on unbacked SymInts). If min/max are None, we assume
  1463. that the dimension is unbounded in that direction. Repeated application
  1464. of constrain_range intersects the ranges. This is a fairly low level API
  1465. that doesn't have a lot of safety guarantees (TODO: provide higher level
  1466. APIs).
  1467. Currently, we use this API in the following circumstance: when we allocate
  1468. an unbacked SymInt, denoting an integer quantity which is data dependent,
  1469. we ordinarily do not know anything about what values it may take. This
  1470. means that any sort of guard on it will immediately fail. However, in
  1471. many cases, we know something about the unbacked SymInt: for example, we
  1472. know that nonzero(x).size(0) must be >= 0. We use constrain_range to
  1473. narrow the possible range, declaring that negative symbols are impossible.
  1474. This permits to definitely answer True to queries like 'nnz >= 0', even if
  1475. we don't know what the actual (hinted) value of 'nnz' is. In fact, we
  1476. actually use constrain_range to unsoundly discharge common guards: for an
  1477. unbacked SymInt produced by nonzero, we will also assume that it is not
  1478. equal to 0/1 (even though these are perfectly possible values at runtime),
  1479. because we generally expect graphs that are valid for N=2 to also be valid
  1480. for N=1.
  1481. """
  1482. if min is None:
  1483. min = -int_oo
  1484. if max is None:
  1485. max = int_oo
  1486. if max < min:
  1487. raise ValueError(
  1488. "Maximum value to constrain_as_size can't be less than the specified min value, "
  1489. f"received min={min} and max={max}"
  1490. )
  1491. if isinstance(a, int):
  1492. if not (min <= a <= max):
  1493. raise ValueError(f"Invalid value {a} for range [{min}:{max}]")
  1494. return
  1495. a.node.shape_env._constrain_range(a.node.expr, min, max)
  1496. def constrain_unify(a: torch.SymInt, b: torch.SymInt) -> None:
  1497. """
  1498. Given two SymInts, constrain them so that they must be equal. NB:
  1499. this will not work with SymInts that represent nontrivial expressions
  1500. (yet!)
  1501. """
  1502. if not isinstance(a, SymInt):
  1503. if not isinstance(b, SymInt):
  1504. if a != b:
  1505. raise AssertionError(f"Expected {a} == {b}")
  1506. return
  1507. else:
  1508. shape_env = b.node.shape_env
  1509. else:
  1510. shape_env = a.node.shape_env
  1511. shape_env._constrain_unify(a, b)
  1512. # Assume that a boolean is true for the purposes of subsequent symbolic
  1513. # reasoning. This will keep track of corresponding runtime checks to verify
  1514. # that the result is upheld: either as a regular guard, or as a special set
  1515. # of asserts which are triggered when an unbacked SymInt is allocated.
  1516. #
  1517. # DO NOT use this function for these cases:
  1518. #
  1519. # - This is inappropriate for "branching" conditions (where both
  1520. # true and false result in valid programs). We will always assume
  1521. # the condition evaluates true, and so it will never be possible
  1522. # to trace the false condition when you use it. For true branching
  1523. # on unbacked SymInts, you must use torch.cond; if you incorrectly
  1524. # use expect_true in this case, you will make the false branch
  1525. # unreachable (as we will simply assume that only the true branch
  1526. # is ever exercised).
  1527. #
  1528. # - This is inappropriate for situations where you know some other system
  1529. # invariant guarantees that this property holds, since you don't
  1530. # really need to insert a runtime check in that case. Use something
  1531. # like constrain_range in that case.
  1532. #
  1533. # This API has a hitch. To avoid having to reimplement error reporting
  1534. # capabilities, this function CAN return False. The invariant is that
  1535. # the surrounding code must raise an error when this function returns
  1536. # False. This is quite low level, so we recommend using other functions
  1537. # like check() which enforce this in a more intuitive way.
  1538. #
  1539. # By the way, this name is a nod to the __builtin_expect macro,
  1540. # which is used similarly (but unlike __builtin_expect, you MUST fail
  1541. # in the unlikely branch.) (I think expect is a good name; in recent
  1542. # versions of C++, this is replaced with [[likely]], which is weaker
  1543. # and not accurate for this function!)
  1544. def expect_true(a: BoolLikeType, skip: int = 0) -> bool:
  1545. if isinstance(a, SymBool):
  1546. # TODO: check perf implications of this
  1547. frame = inspect.currentframe()
  1548. for _ in range(skip + 1): # always run this loop at least once
  1549. if frame is None:
  1550. break
  1551. frame = frame.f_back
  1552. return a.node.expect_true(
  1553. frame.f_code.co_filename if frame else "", frame.f_lineno if frame else 0
  1554. )
  1555. if type(a) is not bool:
  1556. raise AssertionError(f"Expected bool, got {a}")
  1557. return a
  1558. def guard_bool(a: BoolLikeType) -> bool:
  1559. if isinstance(a, SymBool):
  1560. return a.node.guard_bool("", 0) # NB: uses Python backtrace
  1561. if type(a) is not bool:
  1562. raise AssertionError(f"Expected bool, got {a}")
  1563. return a
  1564. def guard_int(a: IntLikeType) -> int:
  1565. if isinstance(a, SymInt):
  1566. return a.node.guard_int("", 0) # NB: uses Python backtrace
  1567. if type(a) is not int:
  1568. raise AssertionError(f"Expected int, got {a}")
  1569. return a
  1570. def guard_float(a: FloatLikeType) -> float:
  1571. if isinstance(a, SymFloat):
  1572. return a.node.guard_float("", 0) # NB: uses Python backtrace
  1573. if not isinstance(a, float):
  1574. raise AssertionError(f"Expected float, got {a}")
  1575. return a
  1576. # Given a GraphModule, return all the FakeTensors for all the placeholders
  1577. def fx_placeholder_vals(gm: torch.fx.GraphModule) -> list[object]:
  1578. return [n.meta["val"] for n in gm.graph.nodes if n.op == "placeholder"]
  1579. def fx_placeholder_targets(gm: torch.fx.GraphModule) -> list[str]:
  1580. return [n.target for n in gm.graph.nodes if n.op == "placeholder"]
  1581. # Given a GraphModule and arguments to run it with, evaluate that the guards
  1582. # for its associated ShapeEnv are satisfied by the passed arguments. This
  1583. # WILL check for duck sizing.
  1584. def eval_guards(
  1585. gm: torch.fx.GraphModule, *args: Tensor, ignore_static: bool = True
  1586. ) -> bool:
  1587. if gm.shape_env is None:
  1588. raise AssertionError("gm.shape_env must not be None")
  1589. return gm.shape_env.evaluate_guards_for_args( # type: ignore[operator, union-attr]
  1590. fx_placeholder_vals(gm), args, ignore_static=ignore_static
  1591. )
  1592. def bind_symbols(gm: torch.fx.GraphModule, *args: Tensor) -> dict[sympy.Symbol, int]:
  1593. if gm.shape_env is None:
  1594. raise AssertionError("gm.shape_env must not be None")
  1595. return gm.shape_env.bind_symbols(fx_placeholder_vals(gm), args) # type: ignore[operator, union-attr]
  1596. class DimDynamic(Enum):
  1597. """
  1598. Controls how to perform symbol allocation for a dimension. It is always
  1599. sound to default this to DYNAMIC, but the policies DUCK and STATIC can
  1600. result in better trace-time and compile-time performance, as they reduce
  1601. the number of allocated symbols and generally make your graph more static.
  1602. NB: If we notice you've applied a constraint to the dimension, we will
  1603. force it to DYNAMIC for simplicity.
  1604. DimDynamic is controlled by a variety of higher level UX features.
  1605. Currently:
  1606. - In eager mode, the default policy is DUCK.
  1607. - The default is changed to STATIC with assume_static_by_default.
  1608. - An individual dim is marked DYNAMIC if you mark_dynamic_dim.
  1609. - In export mode, the default policy is STATIC.
  1610. - An individual dim is marked DYNAMIC if you specify it in
  1611. dynamic_shapes passed to export.
  1612. """
  1613. # Treat the dimension symbolically
  1614. DYNAMIC = 0
  1615. # Treat the dimension symbolically, but if its hint matches another
  1616. # dynamic dimension, unify the two symbols ("duck sizing")
  1617. DUCK = 1
  1618. # Treat the dimension statically based on its hint
  1619. STATIC = 2
  1620. # Treat the dimension as unbacked
  1621. UNBACKED = 3
  1622. # Infer the strides from stride. If size is static, strides will be static as well.
  1623. INFER_STRIDE = 4
  1624. # NB: These constraints affect both clients and backends: given some
  1625. # constraint C, the client must pass inputs that satisfy the constraint,
  1626. # while a backend must not introduce guards BEYOND this constraint.
  1627. # For clarity, we document the implications on both sides for both the client
  1628. # and the backend.
  1629. #
  1630. # NB: These constraints are on a *single* dimension. In principle, we could
  1631. # also have multi-dimension constraints, but our guess is that this is not
  1632. # actually useful and so we are not supporting it right now.
  1633. #
  1634. # NB: Strict constraints are typically only suitable for export, as in eager
  1635. # a backend like inductor may validly introduce extra, discretionary guards
  1636. # to improve performance of code. A StrictMinMaxConstraint would be brittle
  1637. # under future optimizations performed by inductor; we don't guarantee
  1638. # eager code with StrictMinMaxConstraint will keep working in the future!
  1639. @dataclass(frozen=True, slots=True)
  1640. class Constraint:
  1641. warn_only: bool
  1642. @dataclass(frozen=True, slots=True)
  1643. class StrictMinMaxConstraint(Constraint):
  1644. """
  1645. For clients: the size at this dimension must be within 'vr' (which
  1646. specifies a lower and upper bound, inclusive-inclusive) AND it
  1647. must be non-negative and should not be 0 or 1 (but see NB below).
  1648. For backends: there must not be any guards on this dimension which
  1649. are not implied by the given lower and upper bound. Regardless of
  1650. the lower bound, the backend can assume the size is non-negative
  1651. and that it is not 0 or 1.
  1652. An unbounded StrictMinMaxConstraint can be thought of as a strict version
  1653. of "RelaxedUnspecConstraint".
  1654. NB: Export will often unsoundly assume that a graph works for 0/1, even
  1655. though at trace time we assumed size is not 0 or 1. The idea is that
  1656. if we produce a graph that works for a range of values, it will be OK
  1657. for N=0/1 too.
  1658. """
  1659. vr: ValueRanges
  1660. def render(self, source: Source) -> str:
  1661. """Format the constrain equation"""
  1662. # TODO: better printing for -oo and oo
  1663. return f"{self.vr.lower} <= {source.name} <= {self.vr.upper}"
  1664. @dataclass(frozen=True, slots=True)
  1665. class RelaxedUnspecConstraint(Constraint):
  1666. """
  1667. For clients: no explicit constraint; constraint is whatever is implicitly
  1668. inferred by guards from tracing.
  1669. For backends: there must exist at least TWO possible values for the
  1670. size at this dimension which satisfy the guards for this dimension.
  1671. In other words, this constraint helps us distinguish between "we don't
  1672. care if this dimension specializes or not" versus "this dimension must be
  1673. unspecialized." However, this constraint doesn't say very much about what
  1674. specialization is permitted; for example, if we guard on a size being
  1675. even, this would still be acceptable under an unspec constraint. This
  1676. makes RelaxedUnspecConstraint useful for eager mode, where your backend compiler
  1677. may add constraints to otherwise dynamic dimensions; we can't assert that
  1678. there are NO guards as this is brittle because compilers should be able to
  1679. add extra constraints. If you want to assert that there are no guards,
  1680. use StrictMinMaxConstraint with an unbounded ValueRanges.
  1681. """
  1682. def render(self, source: Source) -> str:
  1683. return f"RelaxedUnspecConstraint({source.name})"
  1684. # NB: None here indicates the client constraint is whatever is implicitly
  1685. # inferred by guards from tracing, and that a backend can add whatever guards
  1686. # it wants (including fully specializing the value).
  1687. DimConstraint = Union[StrictMinMaxConstraint, RelaxedUnspecConstraint, None]
  1688. @dataclass(frozen=True, slots=True)
  1689. class EqualityConstraint(Constraint):
  1690. """
  1691. Represent and decide various kinds of equality constraints between input sources.
  1692. A "source pair" is a pair of input sources for dynamic dimensions that
  1693. are specified equal. We represent `source_pairs` in a union-find forest
  1694. so that we can efficiently check whether two such sources are transitively equal.
  1695. A "derived equality" relates an input source to an expression over a root.
  1696. The root can be another input source, corresponding to some dynamic dimension,
  1697. or a phantom symbol that does not directly represent any dynamic dimension. We
  1698. represent `derived_equalities` involving input sources in a transitively-closed map
  1699. so that we can efficiently check whether an input source is transitively equal to
  1700. a given expression over another input source.
  1701. (NOTE: In contrast, it is easy to decide whether an input source is transitively equal
  1702. to a given expression over a phantom symbol; such expressions are already in canonical
  1703. form and so the problem reduces to symbolic expression equality.)
  1704. """
  1705. source_pairs: list[tuple[Source, Source]]
  1706. derived_equalities: list[
  1707. tuple[Source, Union[Source, sympy.Symbol], Callable[[sympy.Expr], sympy.Expr]]
  1708. ]
  1709. phantom_symbols: list[sympy.Symbol]
  1710. relaxed_sources: set[Source]
  1711. _parents: dict[Source, Source] = field(init=False)
  1712. _defs: dict[Source, sympy.Expr] = field(init=False)
  1713. def __post_init__(self) -> None:
  1714. """
  1715. Pre-processing to answer queries `is_equal` and `is_derived` below.
  1716. Example: Suppose we are given:
  1717. source_pairs [a = b, b = c]
  1718. derived_equalities [d = c + 1, e = d - 1]
  1719. We first construct a union find with source_pairs:
  1720. _parents = {a: a, b: a, c: a}
  1721. Then we compute canonical symbolic expressions, recursively applying derived_equalities
  1722. until we bottom out:
  1723. _defs = {d: c + 1, e: (c + 1) - 1 aka c}
  1724. """
  1725. # self._parents is a map from input sources to input sources where, conceptually,
  1726. # these are directed edges in a union-find forest
  1727. _parents: dict[Source, Source] = {}
  1728. object.__setattr__(self, "_parents", _parents)
  1729. # self._defs is a map from input sources to "canonical" symbolic expressions,
  1730. # i.e., unary expressions with symbols that corresponds to regular Dims (i.e.,
  1731. # not derived Dims)
  1732. _defs: dict[Source, sympy.Expr] = {}
  1733. object.__setattr__(self, "_defs", _defs)
  1734. for source1, source2 in self.source_pairs:
  1735. # preprocess into a union-find forest
  1736. self._union(self._find(source1), self._find(source2))
  1737. for source, root, fn in self.derived_equalities:
  1738. # preprocess into a transitively-closed map
  1739. # NOTE(avik): we reuse the union-find forest for canonicalizing input sources
  1740. if isinstance(root, (sympy.Symbol, sympy.Integer)):
  1741. self._defs[self._find(source)] = fn(root)
  1742. else:
  1743. self._defs[self._find(source)] = fn(self._rewrite(root))
  1744. def _find(self, source: Source) -> Source:
  1745. # chase edges to find the root of this equivalence class
  1746. if source in self._parents:
  1747. return self._find(self._parents[source])
  1748. else:
  1749. return source
  1750. def _union(self, root1: Source, root2: Source) -> None:
  1751. # merge two equivalence classes by adding an edge from one root to the other
  1752. if root1 != root2:
  1753. self._parents[root1] = root2
  1754. def _rewrite(self, src: Source) -> sympy.Expr:
  1755. # always represent the given source by the root of its equivalence class
  1756. src = self._find(src)
  1757. if src in self._defs:
  1758. # simply look up the definition if it exists
  1759. # NOTE(avik): This works because definitions are always transitively-closed;
  1760. # otherwise we would have to do recursive rewriting.
  1761. return self._defs[src]
  1762. else:
  1763. # otherwise, create a symbol representing the source
  1764. return sympy.Symbol(src.name)
  1765. def is_equal(self, source1: Source, source2: Source) -> bool:
  1766. return (
  1767. # check whether source1 and source2 have the same root
  1768. # or are relaxed
  1769. (src1 := self._find(source1)) in self.relaxed_sources
  1770. or (src2 := self._find(source2)) in self.relaxed_sources
  1771. or src1 == src2
  1772. # check whether source1 is derived equal to source2
  1773. or self.is_derived(source1, source2, lambda x: x)
  1774. )
  1775. def is_derived(
  1776. self, src: Source, symbol_src: Source, fn: Callable[[sympy.Expr], sympy.Expr]
  1777. ) -> bool:
  1778. # check whether both src and symbol_src have the same definition
  1779. return self._rewrite(src) == fn(self._rewrite(symbol_src))
  1780. def _assert_symbol_context(symbolic_context: object) -> TypeGuard[SymbolicContext]:
  1781. if not isinstance(symbolic_context, SymbolicContext):
  1782. raise AssertionError("Invalid symbolic_context object")
  1783. if type(symbolic_context) is SymbolicContext:
  1784. raise AssertionError("Illegal usage of symbolic_context ABC")
  1785. return True
  1786. def _is_supported_equivalence(
  1787. expr: sympy.Expr,
  1788. ) -> TypeGuard[sympy.Add | sympy.Mul | sympy.Symbol]:
  1789. # Currently supported Dim ops are linear expressions with integer coefficients.
  1790. # So check that expr only contains +, *, ints, and a single occurrence of a symbol.
  1791. # (See also documentation of dynamic_shapes._DerivedDim.)
  1792. if isinstance(expr, (sympy.Add, sympy.Mul)):
  1793. if len(expr.args) > 2:
  1794. return False
  1795. lhs, rhs = expr.args
  1796. return (_is_supported_equivalence(lhs) and isinstance(rhs, sympy.Integer)) or (
  1797. isinstance(lhs, sympy.Integer) and _is_supported_equivalence(rhs)
  1798. )
  1799. return isinstance(expr, sympy.Symbol)
  1800. def _has_uninterpretable_sympy_function(expr: sympy.Basic) -> bool:
  1801. """
  1802. Add functions that our sympy interpreter can't reify into FX nodes
  1803. """
  1804. return expr.has(
  1805. torch.utils._sympy.functions.ToFloat,
  1806. torch.utils._sympy.functions.TruncToInt,
  1807. torch.utils._sympy.functions.CeilToInt,
  1808. )
  1809. @dataclass(frozen=True, slots=True)
  1810. class SymbolicContext:
  1811. """
  1812. Data structure specifying how we should create symbols in
  1813. ``create_symbolic_sizes_strides_storage_offset``; e.g., should
  1814. they be static or dynamic.
  1815. This is an abstract base class because we are probably going to add
  1816. another version of this that says "use exactly these SymInts, don't
  1817. allocate fresh symbols."
  1818. """
  1819. @dataclass(frozen=True, slots=True)
  1820. class SymIntSymbolicContext(SymbolicContext):
  1821. """
  1822. Data structure specifying any constraints on a SymInt input
  1823. """
  1824. constraint: DimConstraint
  1825. _P1 = ParamSpec("_P1")
  1826. _T1 = TypeVar("_T1")
  1827. @dataclass(frozen=True, slots=True)
  1828. class StatelessSymbolicContext(SymbolicContext, Generic[_P1, _T1]):
  1829. """
  1830. Create symbols in ``create_symbolic_sizes_strides_storage_offset`` via
  1831. a symbolic_context determination as given by ``DimDynamic`` and ``DimConstraint``.
  1832. This will cause fresh symbols to be allocated
  1833. """
  1834. dynamic_sizes: DimList[DimDynamic]
  1835. dynamic_strides: DimList[DimDynamic] = None # type: ignore[assignment]
  1836. constraint_sizes: DimList[DimConstraint] = None # type: ignore[assignment]
  1837. constraint_strides: DimList[DimConstraint] = None # type: ignore[assignment]
  1838. specialize_on: Optional[list[list[Callable[_P1, _T1]]]] = None
  1839. # If the tensor is a view, this should be populated for the base. It contains
  1840. # information on how to allocate symbols when recursively fakeifying the base
  1841. # during view fake-ification.
  1842. view_base_context: Optional[SymbolicContext] = None
  1843. # Maps dimension index to shape_id.
  1844. shape_ids: Optional[dict[int, Optional[str]]] = None
  1845. # TODO: add storage offset and stride symbolic_context
  1846. def __post_init__(self) -> None:
  1847. if self.specialize_on is None:
  1848. object.__setattr__(
  1849. self,
  1850. "specialize_on",
  1851. [[]] * len(self.dynamic_sizes),
  1852. )
  1853. if self.dynamic_strides is None:
  1854. object.__setattr__(
  1855. self,
  1856. "dynamic_strides",
  1857. [DimDynamic.INFER_STRIDE] * len(self.dynamic_sizes),
  1858. )
  1859. if self.constraint_sizes is None:
  1860. object.__setattr__(
  1861. self, "constraint_sizes", [None] * len(self.dynamic_sizes)
  1862. )
  1863. if self.constraint_strides is None:
  1864. object.__setattr__(
  1865. self, "constraint_strides", [None] * len(self.dynamic_sizes)
  1866. )
  1867. if not all(
  1868. stride in (DimDynamic.INFER_STRIDE, DimDynamic.DYNAMIC, DimDynamic.DUCK)
  1869. for stride in self.dynamic_strides
  1870. ):
  1871. raise AssertionError(
  1872. "dynamic_strides must only contain INFER_STRIDE, DYNAMIC, or DUCK"
  1873. )
  1874. # note [Tensor Fakification and Symbol Caching]
  1875. #
  1876. # As of the time of this note, dynamo creates a fresh fake tensor mode for backends.
  1877. # The reason we do this is because there are certain classes of operations, namely,
  1878. # metadata mutations, that change tensor size, stride, etc. This means that the fake tensor
  1879. # state at the end of a dynamo trace is different than the fake tensor state at the beginning
  1880. # of a trace. Backends like aot_autograd need a fresh fake tensor to correctly track metadata mutation,
  1881. # view relationships, etc.
  1882. #
  1883. # As we create a new fake mode, we also lose the memoization that comes with it. Rather than
  1884. # transfer the memoization cache, we instead transfer the shape env. However, with this
  1885. # comes nuance - as dynamo is selective in how it makes symbolic shapes. Due to strategies in
  1886. # automatic dynamic and constraints, the policy for which dims are dynamic is nuanced and varies across
  1887. # recompilations.
  1888. #
  1889. # In order to preserve the symbolic decisions made during dynamo tensor fakification, we pass
  1890. # a StatefulSymbolicContext at creation time. This object is tracked, per tensor, on the TracingContext.
  1891. # The lifecycle of this object should match the lifecycle of the original dynamo tracked tensor, and it is
  1892. # safe to reuse this object as many times as necessary to create a fake tensor. Fake tensors
  1893. # created with new fake modes should produce the same exact symbols as the original, providing the same shape_env
  1894. # is used.
  1895. # TODO(voz): Shape env validation
  1896. @dataclass(frozen=True, slots=True, kw_only=True)
  1897. class StatefulSymbolicContext(StatelessSymbolicContext):
  1898. """
  1899. Create symbols in ``create_symbolic_sizes_strides_storage_offset`` via
  1900. a symbolic_context determination as given by a cache of Source:Symbol. A cache hit
  1901. will reuse a stored symbol, and a cache miss will write to this cache.
  1902. This behaves like StatelessSymbolicContext, except the cache supersedes the
  1903. other values - dynamic_sizes and constraint_sizes will not be read if we cache
  1904. hit.
  1905. It is the cache owner's responsibility to maintain the lifecycle of the cache
  1906. with respect to different shape_envs, clearing, etc.
  1907. """
  1908. tensor_source: Source
  1909. # Why is this keyed on int first?
  1910. # That integer is actually the id of the shape_env. This cache short-circuits symbol
  1911. # creation, and we must store it per shape env. Now, while tracing invariants are a single
  1912. # shape env per tracing context, and every new frame gets a new shape_env. So where would we have
  1913. # multiple shape envs? The answer lies in recording. When we are replaying, replay_shape_env_events
  1914. # is invoked, and creates a new shape_env. Replaying events against this new shape_env will
  1915. # cause it to fail with unknown symbols, as the symbols cached here will skip creation, and never
  1916. # get recorded in backed_var_to_val, etc.
  1917. # TODO(voz): consider a weakref to the shape_env here
  1918. shape_env_to_source_to_symbol_cache: dict[int, dict[str, sympy.Expr]] = field(
  1919. default_factory=dict
  1920. )
  1921. @dataclass(frozen=True, slots=True)
  1922. class SubclassSymbolicContext(StatefulSymbolicContext):
  1923. """
  1924. The correct symbolic context for a given inner tensor of a traceable tensor subclass
  1925. may differ from that of the outer symbolic context. This structure allows for this
  1926. flexibility, with inner symbolic contexts mapped via attr -> symbolic context.
  1927. """
  1928. inner_contexts: dict[str, SymbolicContext] = field(default_factory=dict)
  1929. @dataclass(slots=True)
  1930. class TrackedFake:
  1931. """
  1932. Tracks the sources of all fake tensors we wrap in Dynamo.
  1933. Used by shape guard computation.
  1934. """
  1935. fake: FakeTensor | SymInt | SymFloat
  1936. source: Source
  1937. symbolic_context: SymbolicContext | None
  1938. def __hash__(self) -> int:
  1939. return hash((self.fake, self.source.name))
  1940. def __eq__(self, other: object) -> bool:
  1941. if isinstance(other, TrackedFake):
  1942. return self.fake is other.fake and self.source.name == other.source.name
  1943. return False
  1944. def is_symbolic(
  1945. val: Union[int, SymInt, float, SymFloat, bool, SymBool],
  1946. ) -> TypeGuard[Union[SymInt, SymFloat, SymBool]]:
  1947. if isinstance(val, (int, float, bool)):
  1948. return False
  1949. return val.node.is_symbolic()
  1950. IndicatorTypes = (IsNonOverlappingAndDenseIndicator,)
  1951. def _expandsums(args: list[sympy.Expr]) -> tuple[sympy.Expr, bool]:
  1952. """
  1953. Expand products of sums into sums of products.
  1954. This function takes a list of sympy expressions and separates them into
  1955. additive expressions (those with is_Add=True) and other expressions.
  1956. It then computes the distributive product, expanding (a+b)*(c+d) into a*c + a*d + b*c + b*d.
  1957. Args:
  1958. args: A list of sympy expressions to expand
  1959. Returns:
  1960. A tuple containing:
  1961. - The expanded expression as a sympy.Expr
  1962. - A boolean indicating whether expansion occurred (True if multiple additive
  1963. expressions were present or if there was at least one additive and one other expression)
  1964. """
  1965. adds, other = [], []
  1966. for arg in args:
  1967. if arg.is_Add:
  1968. adds.append(arg)
  1969. else:
  1970. other.append(arg)
  1971. result = [sympy.Mul(*other)]
  1972. for add in adds:
  1973. result = [a * b for a, b in itertools.product(result, add.args)]
  1974. result = sympy.Add(*result)
  1975. return result, len(adds) > 1 or (len(adds) > 0 and len(other) > 0)
  1976. def _fast_expand(expr: _SympyT) -> _SympyT:
  1977. """
  1978. A faster implementation of sympy's expand function for common cases.
  1979. This function expands expressions like (a+b)^n or (a+b)*(c+d) into sums of products,
  1980. but avoids the expensive checks and features of sympy's full expand implementation.
  1981. It only recreates objects when necessary to avoid expensive operations.
  1982. Args:
  1983. expr: A sympy expression to expand
  1984. Returns:
  1985. The expanded expression
  1986. """
  1987. # The expand algorithm in sympy is slow due to all the features is supports
  1988. # For eg: e^(-x)*(x-1)/(x+1) is expanded to (x-1)/(e^x + e^x*x) if x is
  1989. # positive and (e^(-x)*x-e^(-x))/(x+1) if x is negative. We do not implement
  1990. # such features here to avoid expensive checks. We also make sure that we
  1991. # only re-create the objects if any of the args changed to avoid expensive
  1992. # checks when re-creating objects.
  1993. new_args = [_fast_expand(arg) for arg in expr.args] # type: ignore[arg-type]
  1994. # pyrefly: ignore [missing-attribute]
  1995. if any(arg is not new_arg for arg, new_arg in zip(expr.args, new_args)):
  1996. # pyrefly: ignore [missing-attribute]
  1997. return _fast_expand(expr.func(*new_args))
  1998. # pyrefly: ignore [missing-attribute]
  1999. if expr.is_Pow:
  2000. base: sympy.Expr
  2001. exp: sympy.Expr
  2002. base, exp = expr.args # type: ignore[assignment]
  2003. if exp.is_Integer and base.is_Add:
  2004. if exp > 1:
  2005. return sympy.expand_multinomial(expr, deep=False)
  2006. elif exp < 0:
  2007. return S.One / sympy.expand_multinomial(S.One / expr, deep=False)
  2008. # pyrefly: ignore [missing-attribute]
  2009. elif expr.is_Mul:
  2010. num: list[sympy.Expr] = []
  2011. den: list[sympy.Expr] = []
  2012. # pyrefly: ignore [missing-attribute]
  2013. for arg in expr.args:
  2014. if arg.is_Pow and arg.args[1] == -1:
  2015. den.append(S.One / arg) # type: ignore[operator, arg-type]
  2016. else:
  2017. num.append(arg) # type: ignore[arg-type]
  2018. num, num_changed = _expandsums(num)
  2019. den, den_changed = _expandsums(den)
  2020. if num_changed or den_changed:
  2021. return num / den
  2022. return expr
  2023. @lru_cache(256)
  2024. def safe_expand(r: _SympyT) -> _SympyT:
  2025. """
  2026. Expand the given symbolic expression by recursively rewriting product of
  2027. sums into sum of products (with the product being either a multiplication or
  2028. exponentiation).
  2029. NOTE: using this on an intermediate expression may prevent simplification
  2030. down the line, e.g., if we eagerly expand `(a + b)^2` into `a^2 + 2ab + b^2`,
  2031. we won't be able to simplify `(a^2 + 2ab + b^2) / (a + b)` as easily.
  2032. """
  2033. if hasattr(r, "expand"):
  2034. try:
  2035. return _fast_expand(r)
  2036. except RecursionError:
  2037. log.warning("RecursionError in _fast_expand(%s)", r)
  2038. return r
  2039. else:
  2040. return r
  2041. class _SymbolInfo(NamedTuple):
  2042. k: sympy.Symbol
  2043. vr: Optional[ValueRanges]
  2044. val: Optional[sympy.Integer]
  2045. is_size_like: bool
  2046. @lru_cache(None)
  2047. def _maybe_evaluate_static_worker(
  2048. expr: _SympyT,
  2049. # NB: this is a tuple to ensure it can be LRU cached
  2050. symbol_info: tuple[_SymbolInfo, ...],
  2051. unbacked_only: bool,
  2052. size_oblivious: bool,
  2053. ) -> Optional[_SympyT]:
  2054. """
  2055. This variant of ShapeEnv._maybe_evaluate_static has no dependence on
  2056. ShapeEnv and thus can be cached indefinitely. It does the "heavy" lifting
  2057. for static evaluation, including nontrivial reliance on Sympy simplification
  2058. that occurs when we reallocate the symbols
  2059. """
  2060. # Simplify making use of value range lower bound
  2061. new_shape_env = {}
  2062. new_range_env = {}
  2063. for idx, sinfo in enumerate(symbol_info):
  2064. k, vr, val, is_size_like = sinfo
  2065. if isinstance(val, SingletonInt):
  2066. # Skip var_ranges logic for SingletonInt which is only used
  2067. # for jagged layout NestedTensors today
  2068. continue
  2069. if vr is None:
  2070. raise AssertionError(f"vr must not be None for symbol {k}")
  2071. if size_oblivious and is_size_like:
  2072. lower = max(2, vr.lower)
  2073. # Clamping size-oblivious to some quantity below sys.maxsize
  2074. # helps us determine that f(u0) != sys.maxsize, which is a
  2075. # test that is looking for sys.maxsize as a sentinel, but you
  2076. # don't really want to worry about it for unbacked SymInts.
  2077. # This is similar to the flavor where size oblivious omits
  2078. # 0/1, it changes semantics but in a benign way.
  2079. upper = min(2**48, vr.upper)
  2080. # Excluding the very upper bound can be helpful
  2081. if upper > lower:
  2082. upper = upper - 1
  2083. # This is a bit dodgy: what this means is that there was a
  2084. # size-like unbacked symbol whose upper bound < 2. This
  2085. # causes... problems.
  2086. if lower <= upper:
  2087. vr = ValueRanges(lower, upper)
  2088. else:
  2089. lower = vr.lower
  2090. # Don't do anything if we don't have a nontrivial lower bound
  2091. # Also don't do anything if we asked only to simplify unbacked
  2092. # SymInt
  2093. if lower is -int_oo or (unbacked_only and val is not None) or not vr.is_int:
  2094. new_range_env[k] = vr
  2095. continue
  2096. # The goal is to take our symbols which have various lower bounds
  2097. # and reallocate them into new symbols which are exactly positive;
  2098. # e.g., if we have s0 in [2, inf], we want to turn it into ess0 in
  2099. # [1, inf], where s0 = ess0 + 1. This gives the most information
  2100. # to sympy for subsequent simplifications.
  2101. #
  2102. # Positive means >= 1
  2103. # Positive - 1 means >= 0
  2104. # Positive + lower - 1 means >= lower
  2105. # The new symbol 's' is "too low", so when we substitute it in
  2106. # we have to increase it by offset (and conversely, the new
  2107. # variables have to have their value range bounds adjusted as
  2108. # well)
  2109. s = sympy.Symbol(f"evaluate_static_shape_{idx}", positive=True, integer=True)
  2110. # Note:
  2111. # Offset might be a fraction(e.g. aten.split.Tensor), but shapes are always integers.
  2112. # Sympy might give unexpected results when comparing an integer with a non-integer
  2113. # Therefore, we cast offset to int here.
  2114. # For example:
  2115. # shape_0 = sympy.Symbol("shape_0", positive=True, integer=True)
  2116. # expr = sympy.Eq(shape_0 - 1/3, 4)
  2117. # expr.xreplace({}) # False
  2118. offset = int(lower - 1)
  2119. new_shape_env[k] = s + offset
  2120. new_range_env[s] = SymPyValueRangeAnalysis.add(vr, -offset)
  2121. # TODO: remove this try catch (esp for unbacked_only)
  2122. try:
  2123. # pyrefly: ignore [missing-attribute]
  2124. new_expr = expr.xreplace(new_shape_env)
  2125. except RecursionError:
  2126. log.warning("RecursionError in sympy.xreplace(%s, %s)", expr, new_shape_env)
  2127. return None
  2128. # We need to canonicalize, as after expand we may have something like `a + b = a` and
  2129. # sympy will not simplify the a. The two appearances of the a will then make value ranges
  2130. # analysis give lose bounds
  2131. new_expr = canonicalize_bool_expr(safe_expand(new_expr))
  2132. if new_expr.is_number:
  2133. return new_expr
  2134. # Check if the range can solve it statically
  2135. out = bound_sympy(new_expr, new_range_env)
  2136. if out.is_singleton():
  2137. return out.lower
  2138. return new_expr if unbacked_only else None
  2139. def error() -> NoReturn:
  2140. raise AssertionError("shouldn't be hit")
  2141. # TODO: Deduplicate this with torch/_prims_common/__init__.py
  2142. def eval_is_non_overlapping_and_dense(
  2143. sizes: Sequence[int], strides: Sequence[int]
  2144. ) -> int:
  2145. return int(guard_bool(_eval_is_non_overlapping_and_dense(sizes, strides)))
  2146. def _eval_is_non_overlapping_and_dense(
  2147. sizes: Sequence[int], strides: Sequence[int]
  2148. ) -> bool:
  2149. """
  2150. Evaluates whether a tensor with the given sizes and strides is non-overlapping and dense.
  2151. A tensor is non-overlapping if there's no memory location that belongs to more than one element.
  2152. A tensor is dense if all elements are stored in memory without gaps.
  2153. Args:
  2154. sizes: Sequence of dimension sizes for the tensor
  2155. strides: Sequence of strides for the tensor
  2156. Returns:
  2157. True if the tensor is non-overlapping and dense, False otherwise
  2158. """
  2159. dim = len(sizes)
  2160. # Short-circuits for tensors of rank one, which are
  2161. # non-overlapping and "dense" if their stride is one
  2162. # or it is a 0/1 element tensor
  2163. if dim == 1:
  2164. return strides[0] == 1 or sizes[0] < 2
  2165. # Checks that there exists a permutation of the strides s.t. the tensor would be contiguous
  2166. # Sorts (length, stride) pairs by stride
  2167. lengths_and_strides = sorted(zip(sizes, strides), key=operator.itemgetter(1))
  2168. # Unlike the C++ code, we don't move the 0/1 size dimensions to the
  2169. # end. So we have to keep going for this code.
  2170. expected_stride = 1
  2171. for length, stride in lengths_and_strides:
  2172. if length == 1:
  2173. continue
  2174. if stride != expected_stride:
  2175. return False
  2176. expected_stride *= length
  2177. return True
  2178. def _sympy_cast_symbool_to_symint_guardless(x: SympyBoolean) -> sympy.Expr:
  2179. return sympy.Piecewise((1, x), (0, True))
  2180. def cast_symbool_to_symint_guardless(
  2181. symbool: Union[bool, torch.SymBool],
  2182. ) -> Union[int, torch.SymInt]:
  2183. """
  2184. Converts a SymBool or bool to a SymInt or int without introducing guards.
  2185. This function maps True to 1 and False to 0, preserving the symbolic nature
  2186. of the input when it's a SymBool. Unlike regular casting which might introduce
  2187. guards, this function performs the conversion without adding any guards.
  2188. Args:
  2189. symbool: A boolean value, either a concrete bool or symbolic SymBool
  2190. Returns:
  2191. The corresponding integer value (1 for True, 0 for False) as either
  2192. a concrete int or symbolic SymInt
  2193. """
  2194. if isinstance(symbool, bool):
  2195. return 1 if symbool else 0
  2196. int_sym = _sympy_cast_symbool_to_symint_guardless(symbool.node.expr)
  2197. return symbool.node.shape_env.create_symintnode(
  2198. int_sym, hint=int(symbool.node.require_hint()) if has_hint(symbool) else None
  2199. )
  2200. SYMPY_INTERP = {
  2201. "IsNonOverlappingAndDenseIndicator": eval_is_non_overlapping_and_dense,
  2202. "cast_symbool_to_symint_guardless": cast_symbool_to_symint_guardless,
  2203. "math": math,
  2204. "torch": torch,
  2205. }
  2206. def _lru_cache(
  2207. fn: Callable[..., _T], maxsize: Optional[int] = None
  2208. ) -> functools._lru_cache_wrapper[_T]:
  2209. """
  2210. Wrapper around lru_cache that clears when new info about shapes has been
  2211. updated.
  2212. Use lru_cache if the output is always the same, regardless of the
  2213. constraints we know now (i.e. evaluate_expr)
  2214. Use _lru_cache otherwise.
  2215. Also note that this depends on _update_version_counter being called on the
  2216. shape environment whenever the constraints are updated, otherwise the cache
  2217. will not be cleared.
  2218. """
  2219. fn_cache = lru_cache(maxsize)(fn)
  2220. prior_version = 0
  2221. if config.validate_shape_env_version_key:
  2222. prior_key = None
  2223. @functools.wraps(fn)
  2224. def wrapper(self: ShapeEnv, *args: Any, **kwargs: Any) -> _T:
  2225. nonlocal prior_version, prior_key
  2226. if prior_key is None:
  2227. prior_key = self._get_key()
  2228. if prior_version != self._version_counter:
  2229. fn_cache.cache_clear()
  2230. prior_version = self._version_counter
  2231. prior_key = self._get_key()
  2232. else:
  2233. if prior_key != self._get_key():
  2234. raise AssertionError(
  2235. "ShapeEnv cache key changed without version being updated!"
  2236. )
  2237. return fn_cache(self, *args, **kwargs)
  2238. else:
  2239. @functools.wraps(fn)
  2240. def wrapper(self: ShapeEnv, *args: Any, **kwargs: Any) -> _T: # type: ignore[misc]
  2241. nonlocal prior_version
  2242. if prior_version != self._version_counter:
  2243. fn_cache.cache_clear()
  2244. prior_version = self._version_counter
  2245. return fn_cache(self, *args, **kwargs)
  2246. wrapper.cache_clear = fn_cache.cache_clear # type: ignore[attr-defined]
  2247. wrapper.cache_info = fn_cache.cache_info # type: ignore[attr-defined]
  2248. return wrapper # type: ignore[return-value]
  2249. @dataclass(frozen=True, slots=True)
  2250. class RuntimeAssert:
  2251. """
  2252. This is pretty similar to ShapeGuard but it also comes with a message,
  2253. and is exclusively used for things that MUST be true (unlike guards,
  2254. which can evaluate False, in which case you just choose not to use
  2255. a particular specialization)
  2256. """
  2257. expr: SympyBoolean
  2258. msg: str = field(repr=False)
  2259. stack: CapturedTraceback = field(repr=False)
  2260. # Used for printing SymExprs in compile_fx
  2261. class SymExprPrinter(PythonPrinter):
  2262. def _print_Float(self, expr: sympy.Float) -> str:
  2263. return str(float(expr))
  2264. class _ShapeGuardPrinter(abc.ABC):
  2265. """
  2266. Abstract base class for printers that convert symbolic expressions to string representations.
  2267. This class provides common functionality for printing symbolic expressions with
  2268. special handling for symbols that represent tensor shapes, strides, etc.
  2269. Subclasses implement specific formatting for different output languages.
  2270. Args:
  2271. symbol_to_source: Mapping from sympy symbols to their source objects
  2272. source_ref: Function to convert a source to its string representation
  2273. var_to_sources: Mapping from sympy symbols to their source objects (for error reporting)
  2274. """
  2275. def __init__(
  2276. self,
  2277. symbol_to_source: Mapping[sympy.Symbol, list[Source]],
  2278. source_ref: Callable[[Source], str],
  2279. var_to_sources: Mapping[sympy.Symbol, list[Source]],
  2280. ) -> None:
  2281. self.symbol_to_source = symbol_to_source
  2282. self.source_ref = source_ref
  2283. self.var_to_sources = var_to_sources
  2284. super().__init__()
  2285. def _print_Float(self, expr: sympy.Float) -> str:
  2286. """Convert a sympy Float to a Python float string representation."""
  2287. return str(float(expr))
  2288. def _print_Symbol(self, expr: sympy.Symbol) -> str:
  2289. """
  2290. Convert a sympy Symbol to its source representation.
  2291. This method looks up the symbol in symbol_to_source mapping and returns
  2292. the string representation of its first source. If the symbol is not in
  2293. symbol_to_source (which can happen when symbols appear in guard expressions
  2294. through simplification or substitution), it falls back to var_to_sources.
  2295. Args:
  2296. expr: The sympy Symbol to convert
  2297. Returns:
  2298. String representation of the symbol's source
  2299. Raises:
  2300. AssertionError: If the symbol is not found in either mapping
  2301. """
  2302. if not isinstance(expr, sympy.Symbol):
  2303. raise AssertionError(f"Expected sympy.Symbol, got {type(expr)}")
  2304. # Try symbol_to_source first, fall back to var_to_sources if not found
  2305. if source := self.symbol_to_source.get(expr):
  2306. return self.print_source(source[0])
  2307. elif source := self.var_to_sources.get(expr):
  2308. return self.print_source(source[0])
  2309. else:
  2310. def repr_sources(src: Mapping[sympy.Symbol, list[Source]]) -> str:
  2311. return repr(
  2312. {
  2313. symbol: [s.name for s in sources]
  2314. for symbol, sources in src.items()
  2315. }
  2316. )
  2317. raise RuntimeError(
  2318. f"{expr} not in {repr_sources(self.symbol_to_source)} or "
  2319. f"{repr_sources(self.var_to_sources)}. This could be due to "
  2320. "the issue described in https://github.com/pytorch/pytorch/pull/90665"
  2321. )
  2322. @abc.abstractmethod
  2323. def print_source(self, source: Source) -> str:
  2324. """
  2325. Convert a source object to its string representation.
  2326. Args:
  2327. source: The source object to convert
  2328. Returns:
  2329. String representation of the source
  2330. """
  2331. ...
  2332. @abc.abstractmethod
  2333. def doprint(self, expr: sympy.Expr) -> str:
  2334. """
  2335. Convert a sympy expression to its string representation.
  2336. Args:
  2337. expr: The sympy expression to convert
  2338. Returns:
  2339. String representation of the expression
  2340. """
  2341. ...
  2342. class ShapeGuardPythonPrinter(_ShapeGuardPrinter, PythonPrinter):
  2343. """
  2344. Python printer for shape guards that extends the base ShapeGuardPrinter.
  2345. This class provides functionality to print symbolic expressions as Python code,
  2346. with caching to improve performance when printing the same expressions multiple times.
  2347. It handles printing of sources and expressions according to Python syntax.
  2348. Args:
  2349. *args: Arguments passed to the parent classes.
  2350. """
  2351. def __init__(self, *args: Any) -> None:
  2352. super().__init__(*args)
  2353. self._print_cache: dict[sympy.Expr, str] = {}
  2354. def print_source(self, source: Source) -> str:
  2355. """
  2356. Convert a source object to its string representation using the source_ref function.
  2357. Args:
  2358. source: The source object to convert
  2359. Returns:
  2360. String representation of the source
  2361. """
  2362. return self.source_ref(source)
  2363. def doprint(self, expr: sympy.Expr) -> str:
  2364. """
  2365. Convert a sympy expression to its Python string representation with caching.
  2366. This method first checks if the expression is already in the cache.
  2367. If found, it returns the cached result; otherwise, it delegates to
  2368. PythonPrinter's doprint method and caches the result.
  2369. Args:
  2370. expr: The sympy expression to convert
  2371. Returns:
  2372. String representation of the expression in Python syntax
  2373. """
  2374. val = self._print_cache.get(expr, None)
  2375. if val is not None:
  2376. return val
  2377. else:
  2378. res = PythonPrinter.doprint(self, expr)
  2379. self._print_cache[expr] = res
  2380. return res
  2381. @deprecated(
  2382. "`torch.fx.experimental.symbolic_shapes.ShapeGuardPrinter` is deprecated, "
  2383. "please use `torch.fx.experimental.symbolic_shapes.ShapeGuardPythonPrinter` instead.",
  2384. category=FutureWarning,
  2385. )
  2386. class ShapeGuardPrinter(ShapeGuardPythonPrinter):
  2387. pass
  2388. class _ShapeGuardCppPrinter(_ShapeGuardPrinter, CppPrinter):
  2389. def __init__(self, *args: Any) -> None:
  2390. self.all_symbols: set[str] = set()
  2391. self.source_to_symbol: dict[Source, sympy.Symbol] = {}
  2392. super().__init__(*args)
  2393. def print_source(self, source: Source) -> str:
  2394. if source in self.source_to_symbol:
  2395. return self.source_to_symbol[source].name
  2396. source_name = source.name
  2397. mangled_name = re.sub("[^0-9a-zA-Z_]+", "_", source_name)
  2398. old_mangled_name = mangled_name
  2399. count = 0
  2400. while mangled_name in self.all_symbols:
  2401. mangled_name = f"{old_mangled_name}_{count}"
  2402. count += 1
  2403. self.source_to_symbol[source] = sympy.Symbol(mangled_name)
  2404. self.all_symbols.add(mangled_name)
  2405. return mangled_name
  2406. def doprint(self, expr: sympy.Expr) -> str:
  2407. return CppPrinter.doprint(self, expr)
  2408. # A dataclass for storing shape guards
  2409. @dataclass(frozen=True, slots=True)
  2410. class _ShapeGuardsHelper:
  2411. exprs: list[str]
  2412. # A dataclass for storing C++ expressions and helper variables
  2413. @dataclass(frozen=True, slots=True)
  2414. class _CppShapeGuardsHelper(_ShapeGuardsHelper):
  2415. source_to_symbol: dict[Source, sympy.Symbol]
  2416. class LoggingShapeGuardPrinter(ShapeGuardPythonPrinter):
  2417. def __init__(self, var_to_sources: Mapping[sympy.Symbol, list[Source]]):
  2418. super().__init__(var_to_sources, lambda n: n.name, var_to_sources)
  2419. class DynamicDimConstraintPrinter(PythonPrinter):
  2420. """
  2421. Printer for dynamic dim constraints.
  2422. - Instead of symbol s_k it prints its source t.size()[i]
  2423. - Instead of Eq(_, _), Mod(_, _), etc. it prints _ == _, _ % _, etc.
  2424. We use this to suggest code for specifying dynamic dim constraints.
  2425. """
  2426. def __init__(
  2427. self,
  2428. symbol_to_source: dict[sympy.Symbol, list[Source]],
  2429. source_name_to_debug_name: Mapping[str, str],
  2430. ):
  2431. super().__init__()
  2432. self.symbol_to_source = symbol_to_source
  2433. self.source_name_to_debug_name = source_name_to_debug_name
  2434. def _print_Symbol(self, expr: sympy.Symbol) -> str:
  2435. if not isinstance(expr, sympy.Symbol):
  2436. raise AssertionError(f"Expected sympy.Symbol, got {type(expr)}")
  2437. if not self.symbol_to_source.get(expr):
  2438. raise AssertionError(f"Unknown symbol {expr} created by constraints solver")
  2439. return self.symbol_to_source[expr][0].name
  2440. class DimConstraints:
  2441. """
  2442. Custom solver for a system of constraints on symbolic dimensions.
  2443. Solutions are "static" values or simplified "dynamic" constraints.
  2444. """
  2445. def __init__(
  2446. self,
  2447. symbol_to_source: dict[sympy.Symbol, list[Source]],
  2448. var_to_val: Mapping[sympy.Symbol, sympy.Integer],
  2449. marked_dynamic: set[sympy.Symbol],
  2450. source_name_to_debug_name: Mapping[str, str],
  2451. ) -> None:
  2452. # We try to solve systems of inequalities with 1 free variable.
  2453. self._univariate_inequalities: dict[sympy.Symbol, set[SympyBoolean]] = (
  2454. defaultdict(set)
  2455. )
  2456. # Among them, we prioritize solving for a free variable that has equalities.
  2457. # NOTE: _symbols_with_equalities is always a subset of _univariate_inequalities.keys()
  2458. # and removing a symbol from the former => removing it from the latter.
  2459. self._symbols_with_equalities: set[sympy.Symbol] = set()
  2460. # A solution of a free variable with equalities becomes a substitution.
  2461. # We use these substitutions to simplify other constraints.
  2462. # NOTE: removing a symbol from _symbols_with_equalities => adding it to _substitutions.
  2463. self._substitutions: dict[sympy.Symbol, sympy.Integer] = {}
  2464. # In general, constraints may have // and % operations.
  2465. # Of course, // can be expressed in terms of / and %.
  2466. # Our inequality solver can handle / but not %. So we need to transform them away.
  2467. # We do so by using the values of variables as hints to evaluate %.
  2468. # For soundness we record additional congruence guards and solve them separately.
  2469. self._var_to_val: Mapping[sympy.Symbol, sympy.Integer] = var_to_val
  2470. self._congruences: defaultdict[sympy.Symbol, set[sympy.Expr]] = defaultdict(set)
  2471. # We do not try to (directly) solve inequalities with > 1 free variables.
  2472. # NOTE: free variables in these inequalities cannot also be in _substitutions.
  2473. self._multivariate_inequalities: set[SympyBoolean] = set()
  2474. # We park external equalities between free variables here.
  2475. self._symbolic_equivalences: list[tuple[Source, sympy.Expr]] = []
  2476. # Solutions come in two forms:
  2477. # - (static) specializations
  2478. # - (dynamic) inequalities / congruences
  2479. self._static_results: set[str] = set()
  2480. self._dynamic_results: set[str] = set()
  2481. # printer for solutions
  2482. self._dcp = DynamicDimConstraintPrinter(
  2483. symbol_to_source, source_name_to_debug_name
  2484. )
  2485. # inconsistencies found on substituting with concrete values / static solutions
  2486. self._inconsistencies: list[str] = []
  2487. # symbols that are marked dynamic
  2488. self._marked_dynamic = marked_dynamic
  2489. # track supported sympy functions and subtract from list of all sympy functions
  2490. self._supported_sympy_functions: set[sympy.Function] = {
  2491. Application,
  2492. Mod,
  2493. PythonMod,
  2494. FloorDiv,
  2495. }
  2496. self._enumerate_sympy_functions()
  2497. def rewrite_with_congruences(self, s: sympy.Symbol, expr: _SympyT) -> _SympyT:
  2498. """
  2499. Eliminate expressions of the form b // d and b % d while adding congruences of the form b % d == k.
  2500. This leaves rational operators (in particular of the form b / d) that our inequality solver can handle.
  2501. We solve the added congruences separately (using our congruence solver, see below).
  2502. """
  2503. def mod_handler(*args: sympy.Expr) -> sympy.Expr:
  2504. # Suppose that we have an expression of the form b % d with free variable s.
  2505. # Using the value of s as a "hint," we can evaluate b % d to a value k.
  2506. # Then we can rewrite b % d to k while adding the guard b % d == k.
  2507. # NOTE(avik): This abstraction is provably sound but, in general, incomplete. It is complete IFF
  2508. # the original expression always evaluates to a constant value (i.e., it does not vary with s).
  2509. # In other words,
  2510. # - solutions of s with the rewritten expression are guaranteed to also be solutions of s with
  2511. # the original expression;
  2512. # - while it may be possible to find solutions of s with the original expression that are not
  2513. # solutions with the rewritten expression, in that case the original expression cannot evaluate
  2514. # to the same value for all solutions of s.
  2515. #
  2516. # Should we be worried about this incompleteness? No, because of the following reasons:
  2517. # 1. It unblocks dramatic simplification that would not be otherwise possible with current tech
  2518. # (i.e., "don't let perfect be the enemy of the good").
  2519. # 2. We already have a tradition of using hints to add guards in the compiler for making progress.
  2520. # 3. We have not yet seen a counterexample arise in practice! In particular, any congruence guards
  2521. # we generate (or simplify to) seem to be of the form b % d == k where k is a constant.
  2522. #
  2523. # Here's a theoretical counterexample: 3*s % (s + 1) == s - 2, that is satisfied by all s >= 2.
  2524. # With any hint (say) s = k, we'd rewrite this to: 3*s % (s + 1) == k - 2. But, substituting, we
  2525. # would then get k - 2 == s - 2, and thus s = k as the (only, constant) solution!
  2526. base, divisor = args
  2527. base, divisor = (
  2528. self.rewrite_with_congruences(s, base),
  2529. self.rewrite_with_congruences(s, divisor),
  2530. )
  2531. mod_reduced = base.xreplace(self._var_to_val) % divisor.xreplace(
  2532. self._var_to_val
  2533. )
  2534. congruence = (base - mod_reduced) % divisor
  2535. if congruence != 0:
  2536. self._congruences[s].add(congruence)
  2537. return mod_reduced
  2538. def floor_div_handler(*args: sympy.Expr) -> sympy.Expr:
  2539. # Suppose that we have an expression of the form b // d with free variable s.
  2540. # Using the value of s, we can evaluate b % d to a value k.
  2541. # Then we can rewrite b // d to (b - k) / d, while adding the guard b % d == k.
  2542. # NOTE(avik): This is exactly equivalent to rewriting b // d as (b - (b % d)) / d
  2543. # and eliminating b % d as above.
  2544. base, divisor = args
  2545. base, divisor = (
  2546. self.rewrite_with_congruences(s, base),
  2547. self.rewrite_with_congruences(s, divisor),
  2548. )
  2549. mod_reduced = base.xreplace(self._var_to_val) % divisor.xreplace(
  2550. self._var_to_val
  2551. )
  2552. congruence = (base - mod_reduced) % divisor
  2553. if congruence != 0:
  2554. self._congruences[s].add(congruence)
  2555. # NB: Must not be CleanDiv, it needs to be regular sympy division
  2556. # so inequality solver works. This is sort of problematic for
  2557. # is_integer tests though haha
  2558. return (base - mod_reduced) / divisor
  2559. # pyrefly: ignore [missing-attribute]
  2560. if expr.has(Mod):
  2561. # pyrefly: ignore [missing-attribute]
  2562. expr = expr.replace(Mod, mod_handler)
  2563. # 7 // -3 is -3, 7 % -3 is -2, and 7 - (-2) / -3 is -3.0 so negative
  2564. # arguments should be OK.
  2565. # pyrefly: ignore [missing-attribute]
  2566. if expr.has(PythonMod):
  2567. # pyrefly: ignore [missing-attribute]
  2568. expr = expr.replace(PythonMod, mod_handler)
  2569. # pyrefly: ignore [missing-attribute]
  2570. if expr.has(FloorDiv):
  2571. # pyrefly: ignore [missing-attribute]
  2572. expr = expr.replace(FloorDiv, floor_div_handler)
  2573. return expr
  2574. def _enumerate_sympy_functions(self) -> None:
  2575. module = torch.utils._sympy.functions
  2576. all_functions = set()
  2577. for attr in dir(module):
  2578. if isinstance(func := getattr(module, attr), sympy.FunctionClass):
  2579. all_functions.add(func)
  2580. self._unsupported_sympy_functions = all_functions.difference(
  2581. self._supported_sympy_functions
  2582. )
  2583. def _has_unsupported_sympy_function(self, expr: sympy.Basic) -> bool:
  2584. """
  2585. Tracks list of sympy.Functions the export solver doesn't know how to handle.
  2586. """
  2587. return expr.has(*self._unsupported_sympy_functions)
  2588. def add(self, expr: SympyBoolean) -> bool:
  2589. """Add an expression to the set of constraints.
  2590. Return whether the expression is a trivial constraint (i.e., an obvious tautology).
  2591. """
  2592. if expr == sympy.true:
  2593. return True
  2594. orig_expr = expr
  2595. orig_reduced = orig_expr.xreplace(self._var_to_val)
  2596. # TODO(avik): https://github.com/pytorch/pytorch/issues/101093
  2597. # It is possible that `expr` will fail the consistency check because of
  2598. # precision errors. Specifically, on substituting its free symbols with
  2599. # their concrete values, we might end up comparing floats. Until we have
  2600. # a fix for this issue, we delay raising such failures. See solve().
  2601. if orig_reduced == sympy.false:
  2602. self._inconsistencies.append(f"{orig_expr} is inconsistent!")
  2603. if isinstance(
  2604. expr, (sympy.Ne, sympy.Or, sympy.And)
  2605. ) or self._has_unsupported_sympy_function(expr):
  2606. # we're not going to do anything useful with these, so drop them
  2607. return False
  2608. free_symbols = expr.free_symbols
  2609. if not free_symbols:
  2610. raise AssertionError(
  2611. f"Did not expect constraint with no free variables: {expr}"
  2612. )
  2613. if len(free_symbols) > 1:
  2614. # multivariate: record and move on
  2615. self._multivariate_inequalities.add(expr)
  2616. else:
  2617. # univariate: can solve these immediately
  2618. s = next(iter(free_symbols))
  2619. # eliminate // and % (see documentation of `rewrite_with_congruences` above)
  2620. old_n_congruences = len(self._congruences[s])
  2621. expr = self.rewrite_with_congruences(s, expr)
  2622. new_n_congruences = len(self._congruences[s])
  2623. if expr == sympy.true:
  2624. return old_n_congruences == new_n_congruences
  2625. reduced = expr.xreplace(self._var_to_val)
  2626. if reduced == sympy.false:
  2627. self._inconsistencies.append(
  2628. f"{expr}, obtained by rewriting {orig_expr} with congruences, "
  2629. "is inconsistent!"
  2630. )
  2631. if isinstance(expr, sympy.Eq):
  2632. # special status for symbols that have equalities (see `solve` below)
  2633. self._symbols_with_equalities.add(s)
  2634. self._univariate_inequalities[s].add(expr)
  2635. return False
  2636. def add_equality(self, source: Source, expr: sympy.Expr) -> None:
  2637. """Add an equality constraint"""
  2638. if expr.is_number:
  2639. # specialization, right here
  2640. self._static_results.add(f"{source.name} == {expr}")
  2641. else:
  2642. # these will resolve to either specializations or dynamic equality constraints
  2643. self._symbolic_equivalences.append((source, expr))
  2644. def _reduce_congruences(self) -> dict[sympy.Symbol, set[sympy.Expr]]:
  2645. reduced_congruences: dict[sympy.Symbol, set[sympy.Expr]] = {}
  2646. for s, congruences in self._congruences.items():
  2647. remainder_modulus_pairs = []
  2648. congruences_to_check = set()
  2649. for congruence in congruences:
  2650. base, divisor = congruence.args
  2651. # We are given a congruence of the form base % divisor == 0 with a free variable s. So:
  2652. # - we transform this into an equation of the form base = divisor * tmp;
  2653. # - we solve this equation for s to get a linear solution with free variable tmp.
  2654. tmp = sympy.Symbol("reduce_congruences_tmp", integer=True)
  2655. symbol, solution = sympy.solve_linear(base - divisor * tmp, symbols=[s])
  2656. # See https://docs.sympy.org/latest/modules/solvers/solvers.html#sympy.solvers.solvers.solve_linear
  2657. # for how to interpret the results.
  2658. if s == symbol:
  2659. # This means the solution is of the form s = modulus*tmp + remainder.
  2660. modulus, remainder = sympy.polys.polytools.div(solution, tmp)
  2661. if isinstance(modulus, sympy.Integer) and isinstance(
  2662. remainder, sympy.Integer
  2663. ):
  2664. # Make sure 0 <= remainder <= modulus.
  2665. remainder = remainder % modulus
  2666. remainder_modulus_pairs.append((remainder, modulus))
  2667. continue
  2668. # This means that we did not get a unique solution to the equation.
  2669. # No problem, we will check it.
  2670. congruences_to_check.add(congruence)
  2671. # Finally we solve for a congruence s such that s = r_i mod m_i for each (r_i, m_i).
  2672. # The solution will be a congruence of the form s = r mod m.
  2673. # NOTE(avik): Since the given m_i may not be pairwise coprime, we can't just use CRT.
  2674. if remainder_modulus_pairs:
  2675. remainder, modulus = sympy.ntheory.modular.solve_congruence(
  2676. *remainder_modulus_pairs
  2677. )
  2678. reduced_congruences[s] = {(s - remainder) % modulus}
  2679. substitution = {
  2680. s: modulus * sympy.Symbol("tmp", integer=True) + remainder
  2681. }
  2682. reduced_congruences[s].update(
  2683. congruence
  2684. for congruence in congruences_to_check
  2685. if not sympy.checksol(congruence, substitution)
  2686. )
  2687. else:
  2688. reduced_congruences[s] = congruences_to_check
  2689. return reduced_congruences
  2690. def _raise_inconsistencies(self) -> None:
  2691. if self._inconsistencies:
  2692. msg = "\n".join(self._inconsistencies)
  2693. self._inconsistencies.clear()
  2694. raise ValueError(f"The following inconsistencies were found:\n{msg}")
  2695. def solve(self) -> None:
  2696. """Solve the system of constraint equations to find simplified constraints"""
  2697. self._raise_inconsistencies()
  2698. # as long as there are symbols with equalities, solve for them
  2699. # NOTE(avik): this is guaranteed to terminate (#iterations <= #symbols)
  2700. while self._symbols_with_equalities:
  2701. s = self._symbols_with_equalities.pop()
  2702. exprs = self._univariate_inequalities.pop(s)
  2703. solution = sympy.solvers.inequalities.reduce_inequalities(exprs, s)
  2704. if isinstance(solution, sympy.And):
  2705. solution = next(
  2706. (arg for arg in solution.args if isinstance(arg, sympy.Eq)),
  2707. solution,
  2708. )
  2709. if not isinstance(solution, sympy.Eq):
  2710. raise AssertionError(
  2711. f"Expected an equality constraint for {s}, got {solution}"
  2712. )
  2713. symbol, val = solution.args
  2714. if symbol != s:
  2715. raise AssertionError(
  2716. f"Expected a constraint on {s} instead of on {symbol}"
  2717. )
  2718. # because this is univariate, the solution is a specialization
  2719. self._static_results.add(
  2720. f"{self._dcp.symbol_to_source[s][0].name} == {val}"
  2721. )
  2722. # add this as a substitution to simplify other constraints
  2723. self._substitutions[s] = val # type: ignore[assignment]
  2724. # simplify multivariate inequalities: some of them will now become univariate!
  2725. multivariate_inequalities = self._multivariate_inequalities
  2726. self._multivariate_inequalities = set()
  2727. for expr in multivariate_inequalities:
  2728. self.add(expr.xreplace({s: self._substitutions[s]}))
  2729. self._raise_inconsistencies()
  2730. # solve linear congruences
  2731. # NOTE(avik): We do not need to solve them for symbols that have already been specialized.
  2732. reduced_congruences = self._reduce_congruences()
  2733. for s, congruences in reduced_congruences.items():
  2734. for congruence in congruences:
  2735. # any congruence that cannot be checked becomes a dynamic constraint as well
  2736. if s not in self._substitutions or not sympy.checksol(
  2737. congruence, {s: self._substitutions[s]}
  2738. ):
  2739. if self._is_supported_congruence(congruence):
  2740. base, divisor = congruence.args
  2741. tmp_name = "_" + str(
  2742. self._dcp.source_name_to_debug_name.get(
  2743. self._dcp.symbol_to_source[s][0].name,
  2744. self._dcp.symbol_to_source[s][0].name,
  2745. )
  2746. )
  2747. tmp = sympy.Symbol(tmp_name, integer=True)
  2748. from torch._dynamo.source import ConstantSource
  2749. self._dcp.symbol_to_source[tmp] = [ConstantSource(tmp_name)]
  2750. r = try_solve(sympy.Eq(base, divisor * tmp), s)
  2751. if r is None:
  2752. raise AssertionError(
  2753. f"Failed to solve {base} = {divisor} * {tmp} for {s}"
  2754. )
  2755. self._dynamic_results.add(self._dcp.doprint(sympy.Eq(s, r[1])))
  2756. # remaining symbols have only pure inequalities (no equalities)
  2757. for s, exprs in self._univariate_inequalities.items():
  2758. try:
  2759. solution = sympy.solvers.inequalities.reduce_inequalities(exprs, s)
  2760. # because this is univariate, the solution is a dynamic (range) constraint
  2761. if isinstance(solution, sympy.Or):
  2762. solution = next(
  2763. iter(
  2764. arg
  2765. for arg in solution.args
  2766. if arg.xreplace(self._var_to_val)
  2767. )
  2768. )
  2769. if isinstance(solution, sympy.And):
  2770. for arg in solution.args:
  2771. self._dynamic_results.add(self._dcp.doprint(arg))
  2772. else:
  2773. self._dynamic_results.add(self._dcp.doprint(solution))
  2774. except (NotImplementedError, AssertionError):
  2775. log.warning("Failed to reduce inequalities", exc_info=True)
  2776. for expr2 in exprs:
  2777. self._dynamic_results.add(self._dcp.doprint(expr2))
  2778. # simplify symbolic equivalences: some of them will now become specializations!
  2779. symbolic_equivalences = self._symbolic_equivalences
  2780. self._symbolic_equivalences = []
  2781. for source, expr3 in symbolic_equivalences:
  2782. self.add_equality(source, expr3.xreplace(self._substitutions))
  2783. # remaining symbolic equivalences become dynamic equality constraints
  2784. for source, expr3 in self._symbolic_equivalences:
  2785. self._dynamic_results.add(f"{source.name} == {self._dcp.doprint(expr3)}")
  2786. @classmethod
  2787. def _is_supported_congruence(cls, congruence: sympy.Expr) -> bool:
  2788. base, divisor = congruence.args
  2789. # Congruences that can be currently expressed with supported Dim ops are
  2790. # of the form (x + a) % b == 0, where x is a Dim and a and b are constants.
  2791. # This allows us to derive x as b*y - a for some Dim y.
  2792. # (See also documentation of dynamic_shapes._DerivedDim.)
  2793. if isinstance(base, sympy.Add):
  2794. lhs, rhs = base.args
  2795. cond = (
  2796. isinstance(lhs, sympy.Symbol) and isinstance(rhs, sympy.Integer)
  2797. ) or (isinstance(lhs, sympy.Integer) and isinstance(rhs, sympy.Symbol))
  2798. else:
  2799. cond = isinstance(base, sympy.Symbol)
  2800. cond = cond and isinstance(divisor, sympy.Integer)
  2801. return cond
  2802. def forced_specializations(self) -> dict[str, sympy.Expr]:
  2803. """Returns a dictionary of the names of symbols to their specialized value"""
  2804. def debug_name(src: Source) -> str:
  2805. name = src.name
  2806. if self._dcp.source_name_to_debug_name:
  2807. return f"{self._dcp.source_name_to_debug_name[name]} = {name}"
  2808. else:
  2809. return name
  2810. return {
  2811. debug_name(self._dcp.symbol_to_source[s][0]): val
  2812. for s, val in self._substitutions.items()
  2813. if s in self._marked_dynamic
  2814. }
  2815. def _is_derived_dim(
  2816. self, dim: object
  2817. ) -> TypeGuard[torch.export.dynamic_shapes._DerivedDim]:
  2818. return isinstance(dim, torch.export.dynamic_shapes._DerivedDim)
  2819. def _is_dim(self, dim: object) -> TypeGuard[torch.export.dynamic_shapes.Dim]:
  2820. return isinstance(dim, torch.export.dynamic_shapes.Dim) and not isinstance(
  2821. dim, torch.export.dynamic_shapes._DerivedDim
  2822. )
  2823. def _process_derived_dim_roots(
  2824. self,
  2825. results: dict[str, dict[str, Any]],
  2826. name_to_dim: dict[str, Any],
  2827. ) -> None:
  2828. """
  2829. Here we resolve 2 concerns with derived dims suggested fixes: 1) newly introduced roots,
  2830. and 2) root swapping.
  2831. 1) Newly introduced roots appear with modulo guards, e.g. Mod(dx, 2) = 0 suggests
  2832. dx is a derived dim equal to 2 * _dx, introducing a new root _dx. Currently the final
  2833. suggested fixes handle this correctly, but we can get intermediate results that look like
  2834. {"dy": {"eq": "dx + 1"}, "dx": {"eq": "2 * _dx + 1, "min": 3, "max": 15}}
  2835. and this routine prettifies this by unifying to a single root, and making each suggestion
  2836. either a derived dim or min/max range, not both.
  2837. 2) With suggested fixes for derived dims, roots can be swapped,
  2838. e.g. dx, dx - 1 -> dy + 1, dy. Here we don't want to print out the attached name,
  2839. since this leads to messages like "dx - 1 = Dim("dx - 1", ...)".
  2840. Instead we evaluate the new root value, and remove results for its derivations.
  2841. First we find all the original roots (specified in dynamic_shapes), that are found in the
  2842. values of results (i.e. used for computing suggesting fix values). These original roots
  2843. (suppose `dx`) are either specialized, unchanged, refined, or swapped
  2844. (expressed as a derived dim). If any of the first 3 cases happen, we suggest `dx`'s value
  2845. in results, and remove suggestions for derivations of `dx`, assuming the derived relation
  2846. is valid. If swapped, we find the new root, and use the fix to evaluate `dx`'s new value,
  2847. and then do the same with `dx`'s derivations.
  2848. Assuming the originally specified derived relations are correct is valid, because:
  2849. 1) if the relations are plain wrong (e.g. input shape = (6, 4) with spec (dx, dx - 1))
  2850. produce_guards() will catch this and crash before hand.
  2851. 2) if the relations are numerically correct but do not match the emitted guard,
  2852. for example:
  2853. def forward(self, x, y):
  2854. return x.reshape([-1]) + y # guard: s0 * 2 = s1
  2855. inputs = (torch.randn(6, 2), torch.randn(12))
  2856. dx = Dim("dx", min=2, max=32)
  2857. dynamic_shapes={"x": (dx, 2), "y": (dx + 6, )} # this matches values but not op
  2858. then this leads to 2 linear equations, and a) produce_guards() is able to solve for
  2859. the unique solution of dx = 6 and specialize, and b) the export constraint solver will
  2860. raise an issue due to range constraints (a unique solution means not all values in a
  2861. range satisfy a guard) and also force specializations.
  2862. """
  2863. from torch.export.dynamic_shapes import Dim
  2864. def _check_same_range(c: Mapping[str, int], dim: object) -> bool:
  2865. # returns True if c & dim are both min/max ranges with same values
  2866. return (
  2867. self._is_dim(dim)
  2868. and ("min" in c or "max" in c)
  2869. and (
  2870. (dim.min < 2 and c.get("min", 2) == 2) or dim.min == c.get("min", 2) # type: ignore[attr-defined]
  2871. ) # let pass if analysis min = 2 and specified min = 0/1
  2872. and dim.max == c.get("max", int_oo) # type: ignore[attr-defined]
  2873. )
  2874. # 1) newly introduced roots
  2875. # this part we handle adding newly introduced roots
  2876. # these arise from guards like "x.shape[0] % 3 == 0"
  2877. # leading to suggested fixes like "dx = 3*_dx"
  2878. # extract _dx, and find appropriate min/max values
  2879. #
  2880. # before, we have something like:
  2881. # {"dx": {"eq": 3*_dx+1, "min": 4, "max": 10}, "dy": dx+1, "dz": dx+2}
  2882. # we want instead:
  2883. # {"_dx": {"min": 1, "max": 4}, "dx": 3*_dx+1, "dy": 3*_dx+2, "dz": 3*_dx+3}
  2884. introduced_roots: dict[str, str] = {} # map new root -> old root
  2885. for k, c in list(results.items()):
  2886. if "eq" in c and isinstance(c["eq"], sympy.Expr): # derived dim
  2887. root = next(iter(c["eq"].free_symbols))
  2888. if str(root) not in name_to_dim:
  2889. introduced_roots[str(root)] = k
  2890. # calculate necessary min & max
  2891. modulus, remainder = sympy.polys.polytools.div(c["eq"], root)
  2892. c_min = c.get("min", 2)
  2893. min_ = math.ceil((c_min - remainder) / modulus)
  2894. c_max = c.get("max", int_oo)
  2895. max_ = math.floor((c_max - remainder) / modulus)
  2896. # create result & dim
  2897. results[str(root)] = {"min": min_, "max": max_}
  2898. name_to_dim[str(root)] = Dim(str(root), min=min_, max=max_)
  2899. # remove old root min/max bounds
  2900. c.pop("min", None)
  2901. c.pop("max", None)
  2902. # alter derivations that depend on old root, to unify to new root
  2903. # e.g. dx=3*_dx+1, dy=dx+1 -> dy=3*_dx+2
  2904. for old_root in introduced_roots.values():
  2905. for c in results.values():
  2906. if (
  2907. "eq" in c
  2908. and isinstance(c["eq"], sympy.Expr)
  2909. and str(symbol := next(iter(c["eq"].free_symbols))) == old_root
  2910. ): # derived dim with root = old_root
  2911. new_root_expr = results[str(old_root)]["eq"] # dx=3*_dx+1
  2912. new_expr = c["eq"].subs({symbol: new_root_expr}) # dy=(3*_dx+1)+1
  2913. c["eq"] = new_expr
  2914. # 2) root swapping
  2915. # collect all the original roots that are used for calculating values of suggested fixes
  2916. # this consists of:
  2917. # 1) {"dx": {"min": ..., "max": ...}} -> dx: refined root dim
  2918. # 2) {"dy": "dx + 1"} -> dx: root for suggested fix
  2919. modified_roots: set[str] = set()
  2920. for k, c in results.items():
  2921. if k not in name_to_dim: # _dynamo.export() may handle source directly
  2922. continue
  2923. if self._is_dim(name_to_dim[k]) and ("min" in c or "max" in c): # case 1)
  2924. modified_roots.add(k)
  2925. elif "eq" in c and isinstance(c["eq"], sympy.Expr): # case 2)
  2926. root = next(iter(c["eq"].free_symbols))
  2927. if root is None:
  2928. raise AssertionError("root must not be None")
  2929. modified_roots.add(str(root))
  2930. # exclude newly introduced roots, we've already processed these
  2931. modified_roots = modified_roots.difference(introduced_roots)
  2932. # evaluate the new value for each root
  2933. # this is now either 1) unchanged, 2) refined with a new range,
  2934. # or 3) specialized to a concrete value
  2935. modified_root_values: dict[str, dict[str, Any]] = {}
  2936. for mroot in modified_roots:
  2937. swapped_root = True
  2938. if mroot in results:
  2939. c = results[mroot]
  2940. if ("min" in c or "max" in c) or isinstance( # range
  2941. c["eq"], int
  2942. ): # specialized
  2943. # here, the original root is a root Dim or concrete value in results.
  2944. # if it is a derived dim, it is swapped, and we handle that below.
  2945. if not _check_same_range(
  2946. c, name_to_dim[mroot]
  2947. ): # ignore if unchanged
  2948. modified_root_values[mroot] = c
  2949. swapped_root = False
  2950. if swapped_root:
  2951. # if the original root has been swapped in results, that means the new root
  2952. # is a range (if it had specialized, the original root would have too).
  2953. # find this new root, and solve for the original root's range.
  2954. for k, c in results.items():
  2955. if k not in name_to_dim:
  2956. continue
  2957. dim = name_to_dim[k]
  2958. if (
  2959. dim.__class__.__name__ == "_DerivedDim"
  2960. and dim.root.__name__ == mroot
  2961. ):
  2962. # only look for min/max root, otherwise root would have specialized
  2963. if "min" in c or "max" in c:
  2964. expr = sympy.sympify(k)
  2965. s = next(iter(expr.free_symbols))
  2966. result = {
  2967. "min": try_solve(sympy.Eq(expr, c["min"]), s)[1], # type: ignore[arg-type, index]
  2968. "max": try_solve(sympy.Eq(expr, c["max"]), s)[1], # type: ignore[arg-type, index]
  2969. }
  2970. if not _check_same_range(
  2971. result,
  2972. name_to_dim[mroot], # type: ignore[index, arg-type]
  2973. ): # ignore if unchanged
  2974. modified_root_values[mroot] = result # type: ignore[index]
  2975. break
  2976. # filter out results where the key is a derived dim (e.g. {"dx - 1" : 4})
  2977. # we only want to suggest fixes for the root, to avoid derived names.
  2978. # also, remove anything in modified_roots, since we either add new modified values after this,
  2979. # or have decided they are unchanged.
  2980. for k in list(results.keys()):
  2981. if k not in name_to_dim:
  2982. continue
  2983. if self._is_derived_dim(name_to_dim[k]) or k in modified_roots:
  2984. del results[k]
  2985. # update results with modified root values
  2986. # now results has the following properties:
  2987. # - only contains original roots as keys
  2988. # - each root is now either specialized, refined, or derived from another original root
  2989. results.update(modified_root_values)
  2990. def prettify_results(
  2991. self,
  2992. original_signature: inspect.Signature,
  2993. dynamic_shapes: Union[dict[str, Any], tuple[Any], list[Any]],
  2994. constraint_violation_error: object,
  2995. forced_specializations: dict[str, str],
  2996. ) -> str:
  2997. """Format a message for constraint violation errors"""
  2998. from torch.export.dynamic_shapes import _get_dim_name_mapping
  2999. if not self._dcp.source_name_to_debug_name:
  3000. # nothing to do
  3001. return ""
  3002. def transform(s: str, inverse: bool = False) -> str:
  3003. for k, v in self._dcp.source_name_to_debug_name.items():
  3004. s = s.replace(k, v) if not inverse else s.replace(v, k)
  3005. return s
  3006. results: defaultdict[str, dict[str, Any]] = defaultdict(dict)
  3007. if dynamic_shapes is None:
  3008. dynamic_shapes = {}
  3009. def flip(op: str) -> str:
  3010. if op == "<=":
  3011. return ">="
  3012. if op == ">=":
  3013. return "<="
  3014. if op == "<":
  3015. return ">"
  3016. if op == ">":
  3017. return "<"
  3018. if op != "==":
  3019. raise AssertionError(f"Expected op to be '==', got {op}")
  3020. return op
  3021. def relation_with_digit(expr: str, op: str, digit: int) -> None:
  3022. if op == "<=":
  3023. results[expr]["max"] = digit
  3024. elif op == "<":
  3025. results[expr]["max"] = digit - 1
  3026. elif op == ">=":
  3027. results[expr]["min"] = digit
  3028. elif op == ">":
  3029. results[expr]["min"] = digit + 1
  3030. else:
  3031. if op != "==":
  3032. raise AssertionError(f"Expected op to be '==', got {op}")
  3033. results[expr]["eq"] = digit
  3034. # retrieve dynamic shapes
  3035. name_to_dim = _get_dim_name_mapping(dynamic_shapes)
  3036. for s in self._static_results.union(self._dynamic_results):
  3037. t = transform(s)
  3038. if t == s:
  3039. continue
  3040. left, op, right = re.split(r"( == | <= | >= | < | > )", t)
  3041. op = op.strip()
  3042. if op == "==" and left == right:
  3043. continue
  3044. if right.isdigit():
  3045. relation_with_digit(left, op, int(right))
  3046. elif left.isdigit():
  3047. relation_with_digit(right, flip(op), int(left))
  3048. else:
  3049. if op != "==":
  3050. raise AssertionError(f"Expected op to be '==', got {op} for {t}")
  3051. try:
  3052. results[left]["eq"] = sympy.sympify(right)
  3053. except TypeError: # rhs source is not linked to Dim name
  3054. pass
  3055. # order forced specializations based on name
  3056. forced_specializations = {
  3057. k: forced_specializations[k]
  3058. for k in sorted(
  3059. forced_specializations.keys(),
  3060. key=lambda x: x.split(" = ")[1],
  3061. )
  3062. }
  3063. buf = ""
  3064. if forced_specializations:
  3065. debug_names = set()
  3066. for k in forced_specializations:
  3067. dim = name_to_dim[k.split(" = ")[0]]
  3068. if self._is_derived_dim(dim):
  3069. debug_names.add(dim.root.__name__) # type: ignore[attr-defined]
  3070. else:
  3071. debug_names.add(dim.__name__)
  3072. buf += (
  3073. f"Specializations unexpectedly required ({', '.join(sorted(debug_names))})! "
  3074. 'For more information, run with TORCH_LOGS="+dynamic".\n'
  3075. )
  3076. for s, val in forced_specializations.items():
  3077. buf += f" - solving the guards generated for {s} resulted in a specialized value of {val}.\n"
  3078. self._process_derived_dim_roots(results, name_to_dim)
  3079. dims = []
  3080. others = []
  3081. # order results by source name
  3082. results2 = {
  3083. k: results[k]
  3084. for k in sorted(
  3085. results.keys(),
  3086. key=lambda x: transform(x, inverse=True),
  3087. )
  3088. }
  3089. for k, c in results2.items():
  3090. if "eq" in c:
  3091. other = c["eq"]
  3092. if isinstance(other, int):
  3093. others.append(f"{k} = {other}")
  3094. elif _is_supported_equivalence(other):
  3095. others.append(f"{k} = {other}")
  3096. else:
  3097. min_ = c.get("min", None)
  3098. if min_ == 2:
  3099. min_ = None
  3100. max_ = c.get("max", None)
  3101. if min_ is not None and max_ is not None:
  3102. dims.append(f"{k} = Dim('{k}', min={min_}, max={max_})")
  3103. elif min_ is not None:
  3104. dims.append(f"{k} = Dim('{k}', min={min_})")
  3105. elif max_ is not None:
  3106. dims.append(f"{k} = Dim('{k}', max={max_})")
  3107. else:
  3108. dims.append(f"{k} = Dim('{k}')")
  3109. # results2 will get filtered out if no new suggestions,
  3110. # this can happen if guards are too complex.
  3111. # in that case don't suggest fix
  3112. if dims or others:
  3113. buf += "\nSuggested fixes:\n "
  3114. buf += "\n ".join(dims + others)
  3115. return buf
  3116. TLS = threading.local()
  3117. @dataclass(frozen=True, slots=True)
  3118. class ShapeEnvSettings:
  3119. """
  3120. Encapsulates all shape env settings that could potentially affect
  3121. FakeTensor dispatch. Used when creating dispatch cache keys.
  3122. """
  3123. allow_scalar_outputs: bool
  3124. allow_dynamic_output_shape_ops: bool
  3125. assume_static_by_default: bool
  3126. specialize_zero_one: bool
  3127. duck_shape: bool
  3128. prefer_deferred_runtime_asserts_over_guards: bool
  3129. trace_asserts: bool
  3130. @dataclass(slots=True)
  3131. class ValueRangesSLoc:
  3132. """
  3133. Locations of the guards that triggered lower and upper bound.
  3134. """
  3135. lower: SLoc
  3136. upper: SLoc
  3137. @contextmanager
  3138. def _suppress_guards(shape_env: ShapeEnv) -> Iterator[None]:
  3139. shape_env._suppress_guards_enter()
  3140. try:
  3141. yield
  3142. finally:
  3143. shape_env._suppress_guards_exit()
  3144. @dataclass(slots=True)
  3145. class _FrameLocalResult:
  3146. loc: Optional[str] = None
  3147. locals: dict[str, Any] = field(default_factory=dict)
  3148. symbols: dict[str, str] = field(default_factory=dict)
  3149. class ShapeEnv:
  3150. # This is a wrapper over the actual __init__ function.
  3151. #
  3152. # Where to add a new constructor parameter to ShapeEnv?
  3153. # =====================================================
  3154. # This __init__ function should be used only for parameters related to event recording.
  3155. # These are parameters that we don't wish to pass down the road to new ShapeEnv instances
  3156. # created from replaying events.
  3157. #
  3158. # If you wish to add a parameter to the constructor of ShapeEnv, unrelated to event
  3159. # recording, do so in the _init function.
  3160. def __init__(
  3161. self,
  3162. *,
  3163. should_record_events: Optional[bool] = None,
  3164. tracked_fakes: Optional[list[Any]] = None,
  3165. **kwargs: Any,
  3166. ) -> None:
  3167. self._init(**kwargs)
  3168. # Disable event recording when replaying.
  3169. kwargs["should_record_events"] = False
  3170. from torch.fx.experimental.validator import translation_validation_enabled
  3171. self._translation_validation_enabled = translation_validation_enabled()
  3172. # If not specified, enable event recording if both:
  3173. # - Translation validation is on
  3174. # - Translation validation bisection is not disabled
  3175. self.should_record_events = (
  3176. should_record_events
  3177. if should_record_events is not None
  3178. else (
  3179. self._translation_validation_enabled
  3180. and not config.translation_validation_no_bisect
  3181. )
  3182. )
  3183. # Enable event recording check if both:
  3184. # - It should record events
  3185. # - The recording check is enabled
  3186. self.check_recorded_events = (
  3187. self.should_record_events and config.check_shape_env_recorded_events
  3188. )
  3189. # This will make sure we only record the top-level function call.
  3190. self.is_recording = False
  3191. # Keep track of the list of tracked fakes.
  3192. self.tracked_fakes = tracked_fakes
  3193. # List of events for reconstructing ShapeEnv at arbitrary points in time.
  3194. self.events: list[ShapeEnvEvent] = (
  3195. [ShapeEnvEvent(ShapeEnv, kwargs=kwargs)]
  3196. if self.should_record_events
  3197. else []
  3198. )
  3199. # FakeTensor per-ShapeEnv operation cache. This is used for caching
  3200. # operations that contain symbolic shapes which have guards on the
  3201. # ShapeEnv (so are ShapeEnv-dependent).
  3202. #
  3203. # NOTE: It's important that SymNodes in this cache have their ShapeEnv
  3204. # stripped otherwise you end up with cycles which can only be cleaned
  3205. # with the GC.
  3206. self.fake_tensor_cache: dict[
  3207. torch._subclasses.fake_tensor._DispatchCacheKey,
  3208. torch._subclasses.fake_tensor._DispatchCacheEntry,
  3209. ] = {}
  3210. # Pro-tip: if you add new field to ShapeEnv, this affects some accept
  3211. # tests. Accept their output with:
  3212. #
  3213. # EXPECTTEST_ACCEPT=1 python test/dynamo/test_dynamic_shapes.py -k test_shape_env_equal
  3214. #
  3215. def _init(
  3216. self,
  3217. *,
  3218. allow_scalar_outputs: bool = True,
  3219. allow_dynamic_output_shape_ops: bool = True,
  3220. # NB: These are legacy configuration that help us make good choices
  3221. # when the constraint/dynamic dims are not explicitly passed to us.
  3222. # Ideally we will fix all call sites to be explicit and not have
  3223. # implicit choices, but this apparently was pretty involved.
  3224. assume_static_by_default: bool = False,
  3225. # Note - On 0/1 specialization
  3226. #
  3227. # The following options affect decisions we make about eager
  3228. # specialization. Disabling them will increase trace time (as we do
  3229. # more symbolic reasoning) and can also harm the quality of generated
  3230. # code (because inductor may not be able to specialize for bounds
  3231. # being equal--although if we later respecialize because of a guard,
  3232. # your code may be just as good as it was before.)
  3233. #
  3234. # When True, eagerly specialize input sizes which have 0/1.
  3235. specialize_zero_one: bool = True,
  3236. # When True, assume input sizes which have the same size are
  3237. # symbolically equal.
  3238. duck_shape: Optional[bool] = None,
  3239. # For debugging
  3240. co_fields: Optional[dict[str, str]] = None,
  3241. # When True, whenever safe, we will generate a deferred runtime assert
  3242. # instead of a guard whenever we know that an expression must be True,
  3243. # otherwise it would be an error, even for backed SymInts (where we
  3244. # could ostensibly unconditionally generate guards). This is useful
  3245. # for export, where preventing "error checking" sizes from showing up
  3246. # in guards is helpful, since these guards in some sense are overly
  3247. # pedantic. See also https://github.com/pytorch/pytorch/issues/121749
  3248. prefer_deferred_runtime_asserts_over_guards: bool = False,
  3249. # XXX Add any new settings that could affect FakeTensor evaluation
  3250. # to: torch._subclasses.fake_tensor._ShapeEnvSettings
  3251. trace_asserts: bool = False,
  3252. ) -> None:
  3253. if duck_shape is None:
  3254. duck_shape = config.use_duck_shape
  3255. self.settings = ShapeEnvSettings(
  3256. # Not directly used by ShapeEnv; indirectly used by FakeTensor
  3257. allow_scalar_outputs=allow_scalar_outputs,
  3258. allow_dynamic_output_shape_ops=allow_dynamic_output_shape_ops,
  3259. # End
  3260. assume_static_by_default=assume_static_by_default,
  3261. specialize_zero_one=specialize_zero_one,
  3262. duck_shape=duck_shape,
  3263. prefer_deferred_runtime_asserts_over_guards=prefer_deferred_runtime_asserts_over_guards,
  3264. trace_asserts=trace_asserts,
  3265. )
  3266. self.guards: list[ShapeGuard] = []
  3267. self.axioms: dict[sympy.Expr, sympy.Expr] = {}
  3268. # A set of ids that have already been allocated. This is used
  3269. # for when we allocate symbol ids using the hash of the source
  3270. # names to ensure we don't have collisions via linear probing
  3271. self.unique_ids: set[int] = set()
  3272. # Maps symbolic ints to their original concrete values
  3273. # Currently populated from tensors
  3274. # When hint is overridden in mark_dynamic, the value stored here
  3275. # is the overridden hint (this is the source of truth for backed
  3276. # hints). The override is also recorded in var_to_hint_override
  3277. # so it can be included in the FxGraphCache key.
  3278. self.backed_var_to_val: dict[sympy.Symbol, sympy.Integer] = {}
  3279. # Only set when propagate_real_tensors is on.
  3280. # Used as last resort to avoid GuardOnDataDependent error in draft export.
  3281. self.real_tensor_prop_unbacked_vals: dict[sympy.Symbol, sympy.Integer] = {}
  3282. # Maps symbolic ints to their min/max range. These ranges
  3283. # are conservative: the int MUST fall in the range, but the
  3284. # range may contain ints which may not actually appear in
  3285. # practice
  3286. self.var_to_range: dict[sympy.Symbol, ValueRanges] = {}
  3287. self.var_to_range_sloc: dict[sympy.Symbol, ValueRangesSLoc] = {}
  3288. self.source_name_to_debug_name: dict[str, str] = {}
  3289. self.var_to_sources: dict[sympy.Symbol, list[Source]] = {}
  3290. # A set of unbacked symbols that are inputs (i.e: not data dependent).
  3291. self.unbacked_inputs: OrderedSet[sympy.Symbol] = OrderedSet()
  3292. self.var_to_stack: dict[sympy.Symbol, CapturedTraceback] = {}
  3293. # User-provided hint overrides from mark_dynamic/mark_unbacked.
  3294. # Even though we never read hints for backed variables from this
  3295. # dict (backed hints are read from backed_var_to_val), we still
  3296. # want them to always be stored here, since this dict is used as
  3297. # part of the FxGraphCache key.
  3298. self.var_to_hint_override: dict[sympy.Symbol, int] = {}
  3299. # Maps a source to the *original* symbol that was assigned to it
  3300. self.source_to_var: dict[str, sympy.Symbol] = {}
  3301. # Maps from sympy ints to expressions representing them
  3302. # Populated from equality guards (i.e. a.shape[0] == b.shape[0])
  3303. self.replacements: dict[sympy.Symbol, sympy.Expr] = {}
  3304. # The sloc of the guard that triggered this replacement to be added
  3305. self.replacements_slocs: dict[sympy.Symbol, SLoc] = {}
  3306. self.unbacked_renamings: dict[sympy.Symbol, sympy.Symbol] = {}
  3307. # Set holds a % b expressions that evaluate to 0.
  3308. self.divisible: set[sympy.Expr] = set()
  3309. # Set that holds "size-like" symbols. When we perform
  3310. # "size-oblivious" tests, these can be assumed to be >= 2.
  3311. self.size_like: set[sympy.Symbol] = set()
  3312. # Duck-shaping says that if two input tensors have the same size,
  3313. # they get assigned the same symbolic variable
  3314. self.val_to_var: dict[int, sympy.Symbol] = {}
  3315. self.unbacked_symfloat_counter = 0
  3316. self.unbacked_symint_counter = 0
  3317. # Similar to guards, but these MUST evaluate to true and can
  3318. # only be evaluated at runtime midway through (i.e., they always
  3319. # involve unbacked symints)
  3320. #
  3321. # For efficiency reasons, we index in the following way. Suppose you have
  3322. # a runtime assert i0 + i1 <= s1. We pick the most recently allocated
  3323. # symbol in the source expression and add the assert to the list for
  3324. # that symbol e.g., {i1: [i0 + i1 <= s1]}.
  3325. #
  3326. # We access the runtime asserts in two situations:
  3327. #
  3328. # - When we are guarding on an expression, we will attempt to
  3329. # statically evaluate it, in case the unbacked SymInts can
  3330. # simplify away. If we have a runtime assert, we may be able
  3331. # to discharge the guard entirely. We only need to attempt
  3332. # runtime asserts that mention freevars of the expression in
  3333. # question.
  3334. #
  3335. # - When we are performing codegen (in Inductor for eager, or
  3336. # when finalizing the export FX graph), we need to know what
  3337. # extra runtime asserts to insert. Whenever an unbacked
  3338. # SymInt comes into scope, all runtime asserts involving it
  3339. # become eligible for insertion (so long as all of their other
  3340. # free unbacked symbols are also in scope). We technically
  3341. # can handle any choice of key by kicking inexpressible asserts
  3342. # to the next unbacked symbol to wait on, but if we choose the
  3343. # latest key, an assert will only show up at the moment when
  3344. # we can actually codegen it.
  3345. self.deferred_runtime_asserts: dict[
  3346. Optional[sympy.Symbol], list[RuntimeAssert]
  3347. ] = {}
  3348. # This exists so we can efficiently invalidate the cache (it's used as
  3349. # part of the cache key); otherwise we'd have to iterate through
  3350. # deferred_runtime_asserts to compute its length
  3351. self.num_deferred_runtime_asserts = 0
  3352. self.log = log
  3353. self.log.info("create_env")
  3354. self.frozen = False
  3355. self.runtime_asserts_frozen = False
  3356. self.dim_constraints: Optional[DimConstraints] = None
  3357. self.counter: Counter[str] = collections.Counter()
  3358. # Mapping from sympy.Symbol to the number of guards which mention this
  3359. # symbol
  3360. self.symbol_guard_counter: Counter[sympy.Symbol] = collections.Counter()
  3361. # A selection of important fields on co_field; solely used for
  3362. # signpost_event
  3363. self.co_fields = co_fields if co_fields else {}
  3364. # Whenever we allocate a fresh unbacked Symbol, we add it to this
  3365. # pending list. Unbacked symbol allocation can occur at unpredictable
  3366. # points during meta tensor propagation, but at some point, we
  3367. # have to know what the binding site for an unbacked symbol is, and
  3368. # this is computed when we actually place the node in the graph. The
  3369. # important thing is that we always actually handle every unaccounted
  3370. # for unbacked symbol, so this list helps us keep track of them and
  3371. # then make sure they are all accounted for.
  3372. #
  3373. # We could potentially give rise to errors earlier by lexically
  3374. # scoping when we do propagation, and only allowing unbacked symbols
  3375. # to be allocated at this point in time. However this is inconvenient
  3376. # to do in Dynamo, because fake tensor propagation is far from when we
  3377. # analyze binding sites (set_example_value), so we do it in a more
  3378. # mutatey way.
  3379. #
  3380. # NB: fresh unbacked symbols NEVER get substitutions applied to them,
  3381. # they are binding sites!
  3382. self.pending_fresh_unbacked_symbols: list[sympy.Symbol] = []
  3383. # These are symbols which we'd like to process as pending, but if
  3384. # they're missing then it's okay too.
  3385. self.ignorable_fresh_unbacked_symbols: list[sympy.Symbol] = []
  3386. # Version counter used to invalidate cached values
  3387. self._prev_cache_key = self._get_key()
  3388. self._version_counter = 0
  3389. # Each time divisible is changed this should be set to True, this is set in _update_version_counter.
  3390. self._resimplify_floor_div_axioms = True
  3391. # Cache for FX nodes.
  3392. # Maps an already built node a tuple of:
  3393. # 1. node's target
  3394. # 2. list of arguments
  3395. # This drastically reduces the size of the FX graph, avoiding
  3396. # duplicated nodes.
  3397. self.fx_node_cache: dict[tuple[Callable, tuple[Any, ...]], torch.fx.Node] = {}
  3398. self.source_to_symbol: dict[str, sympy.Symbol] = {}
  3399. # Suppose you want to replace an unbacked symbol with another
  3400. # unbacked symbol. This is error prone because you can cause
  3401. # references to unbacked symbols to time travel backwards. E.g.,
  3402. #
  3403. # u1 = x.item()
  3404. # ... use of u1 ...
  3405. # u2 = y.item()
  3406. # u3 = z.item()
  3407. # torch._check(u1 == u2 + u3)
  3408. #
  3409. # If you replace u1 with u2 + u3, then the use of u1 now
  3410. # references u2 and u3 prior to them actually being bound at
  3411. # runtime.
  3412. #
  3413. # To control for this, we track the order unbacked symbols
  3414. # were allocated, and only allow substitutions if they respect
  3415. # the dependency from this order; an unbacked symbol can only
  3416. # be substituted with unbacked symbols that come before it in the
  3417. # order.
  3418. #
  3419. # This also imposes an ordering on the unbacked symbol binding
  3420. # sites themselves: you are not allowed to reorder unbacked symbol
  3421. # bindings. At the moment, this is not tracked, but we potentially
  3422. # could track this at the IR level using a higher order operator
  3423. # with something like effect token tracking.
  3424. self.unbacked_alloc_order: dict[sympy.Symbol, int] = {}
  3425. self.specialization_stacks: dict[Source, traceback.StackSummary] = {}
  3426. self.trace_asserts = trace_asserts
  3427. self.specializations: OrderedSet[Specialization] = OrderedSet()
  3428. from torch.fx.experimental.validator import translation_validation_enabled
  3429. self._translation_validation_enabled = translation_validation_enabled()
  3430. if self._translation_validation_enabled:
  3431. from torch.fx.experimental.validator import TranslationValidator
  3432. self.validator = TranslationValidator()
  3433. self.graph = torch.fx.Graph()
  3434. # Create an output graph and start inserting before that.
  3435. # This is needed when 'deepcopy'-ing this object.
  3436. self.graph.inserting_before(self.graph.output(None))
  3437. # Mapping of each node name to the node itself.
  3438. #
  3439. # This is useful for matching an FX node from a recorded ShapeEnv.graph
  3440. # to the FX node of the ShapeEnv we are running the event on.
  3441. #
  3442. # Whenever you add a node to self.graph, you must add a mapping to this
  3443. # variable. Otherwise, the built FX graph on the replayed ShapeEnv will
  3444. # not be valid.
  3445. self.name_to_node: dict[str, torch.fx.Node] = {}
  3446. # Maps shape_id to the first unbacked symbol allocated for that id.
  3447. # When mark_unbacked is called with a shape_id, we allocate fresh
  3448. # symbols but add runtime equality checks via torch._check to ensure
  3449. # all dims with the same shape_id are treated as the same symbol.
  3450. self._shape_id_to_unbacked_symbol: dict[str, sympy.Expr] = {}
  3451. @property
  3452. def allow_scalar_outputs(self) -> bool:
  3453. return self.settings.allow_scalar_outputs
  3454. @property
  3455. def allow_dynamic_output_shape_ops(self) -> bool:
  3456. return self.settings.allow_dynamic_output_shape_ops
  3457. @property
  3458. def assume_static_by_default(self) -> bool:
  3459. return self.settings.assume_static_by_default
  3460. @property
  3461. def specialize_zero_one(self) -> bool:
  3462. return self.settings.specialize_zero_one
  3463. @property
  3464. def duck_shape(self) -> bool:
  3465. return self.settings.duck_shape
  3466. @property
  3467. def prefer_deferred_runtime_asserts_over_guards(self) -> bool:
  3468. return self.settings.prefer_deferred_runtime_asserts_over_guards
  3469. @contextmanager
  3470. def patch_source_specialization(
  3471. self, source: Source, check_fn: Callable[[sympy.Symbol], sympy.Expr]
  3472. ) -> Iterator[None]:
  3473. """
  3474. Temporarily add symbol-level axioms to the ShapeEnv. This is useful when you want to "fork"
  3475. and have parallel universes of ShapeEnvs. For example, we use this when doing multi-graph
  3476. compile so we can support various graphs with varying levels of specializations.
  3477. This context manager allows for temporarily adding constraints to the shape environment
  3478. based on a specialization function applied to a symbol associated with a source.
  3479. Args:
  3480. source: The source of the symbol to specialize
  3481. check_fn: A function that takes a sympy Symbol and returns a sympy expression
  3482. representing a constraint/specialization to be applied
  3483. """
  3484. name = source.name
  3485. sym = self.source_to_var[name]
  3486. expr = check_fn(SymInt(SymNode(sym, self, int, None))).node._expr
  3487. new_axioms = dict(self.get_implications(self.simplify(expr)))
  3488. added_replacements = {}
  3489. for axiom in new_axioms:
  3490. if (
  3491. isinstance(axiom, sympy.Eq)
  3492. and isinstance(axiom.lhs, sympy.Symbol)
  3493. and isinstance(axiom.rhs, sympy.Integer)
  3494. and axiom.lhs not in self.replacements
  3495. ):
  3496. self.replacements[axiom.lhs] = axiom.rhs
  3497. added_replacements[axiom.lhs] = axiom.rhs
  3498. self.axioms.update(new_axioms)
  3499. # We need to freeze the ShapeEnv because any additional modification of
  3500. # the ShapeEnv will cause unsoundness for subsequent specialization calls.
  3501. self.frozen = True
  3502. try:
  3503. yield
  3504. finally:
  3505. for k in new_axioms:
  3506. self.axioms.pop(k, None)
  3507. for k in added_replacements:
  3508. self.replacements.pop(k, None)
  3509. self.frozen = False
  3510. def check_equal(self, other: ShapeEnv) -> None:
  3511. """Compare another ShapeEnv for equivalence"""
  3512. # ShapeEnv fields that are not relevant for the outcome of
  3513. # ShapeEnv.produce_guards call:
  3514. # - Debugging variables
  3515. # - Translation validation related variables
  3516. # - Events recording related variables
  3517. non_state_variable_names = (
  3518. "counter",
  3519. "log",
  3520. "var_to_stack",
  3521. "fx_node_cache",
  3522. "graph",
  3523. "validator",
  3524. "check_recorded_events",
  3525. "should_record_events",
  3526. "is_recording",
  3527. "tracked_fakes",
  3528. "events",
  3529. "source_name_to_debug_name",
  3530. "_prev_cache_key",
  3531. "_version_counter",
  3532. "dim_constraints",
  3533. # source locations are OK to diverge
  3534. "var_to_range_sloc",
  3535. "replacements_slocs",
  3536. "_resimplify_floor_div_axioms",
  3537. "_expr_sym_node_id",
  3538. "specialization_stacks",
  3539. )
  3540. # Mapping of the value of each to-be-compared field into the values that
  3541. # should actually be compared.
  3542. #
  3543. # You should modify this if, for example, the field that holds state and
  3544. # debugging information. e.g. ShapeGuard holds the actual guard (sympy.Expr)
  3545. # and the stack when it was added to the set of guards. In order to compare
  3546. # it, we throw away the stack information.
  3547. def map_value(key: str, value: Any) -> Any:
  3548. if key == "guards":
  3549. # Transform the list of ShapeGuard into a list of expressions.
  3550. return [g.expr for g in value]
  3551. elif key == "deferred_runtime_asserts":
  3552. # Transform the list of RuntimeAsserts into a list of expressions.
  3553. return {s: [ra.expr for ra in ras] for s, ras in value.items()}
  3554. elif key == "name_to_node":
  3555. # Compare just the set of keys is the same.
  3556. return set(value.keys())
  3557. elif key in (
  3558. "symbol_guard_counter",
  3559. "pending_fresh_unbacked_symbols",
  3560. "fake_tensor_cache",
  3561. ):
  3562. # Skip this for comparisons
  3563. return None
  3564. return value
  3565. shape_env_check_state_equal(self, other, non_state_variable_names, map_value)
  3566. def _snapshot_tracked_fakes(self) -> Optional[list[Any]]:
  3567. if self.tracked_fakes is None:
  3568. return None
  3569. from torch._dynamo.variables.builder import TrackedFake
  3570. def maybe_transform_fake(fake: TrackedFake) -> TrackedFake:
  3571. inner_fake = (
  3572. fake.fake
  3573. if isinstance(fake.fake, (torch.SymInt, torch.SymFloat))
  3574. else FakeTensorMeta.from_fake(fake.fake)
  3575. )
  3576. # Even though TrackedFake accepts either a Union[SymInt, FakeTensor], here we give it a
  3577. # FakeTensorMeta for two reasons:
  3578. # 1. this is all the information we need when recording ShapeEnvEvents.
  3579. # 2. it works even if each TrackedFake changes its metadata.
  3580. return TrackedFake(inner_fake, fake.source, fake.symbolic_context) # type: ignore[arg-type]
  3581. return [maybe_transform_fake(fake) for fake in self.tracked_fakes]
  3582. def _last_event_index(self) -> int:
  3583. return len(self.events) - 1
  3584. @contextmanager
  3585. def _recording(self) -> Iterator[None]:
  3586. self.is_recording = True
  3587. try:
  3588. yield
  3589. finally:
  3590. self.is_recording = False
  3591. @record_shapeenv_event()
  3592. def _eliminate_unbacked(self, orig_s: sympy.Symbol, new_s: sympy.Expr) -> None:
  3593. self._set_replacement(orig_s, new_s, "eliminate_unbacked")
  3594. @record_shapeenv_event()
  3595. def set_real_tensor_prop_unbacked_vals(self, k: sympy.Symbol, v: int) -> None:
  3596. """Used only when propagate_real_tensors; registers a value for an
  3597. unbacked symbol, which can be used last resort to resolve hints."""
  3598. log.info("set_real_tensor_prop_unbacked_vals %s = %s", k, v)
  3599. self.real_tensor_prop_unbacked_vals[k] = sympy.sympify(v)
  3600. # Unlike set_replacement, this records a shapeenv event
  3601. @record_shapeenv_event()
  3602. def _rename_unbacked_to(self, orig_s: sympy.Symbol, new_s: sympy.Symbol) -> None:
  3603. if not isinstance(orig_s, sympy.Symbol):
  3604. raise AssertionError(f"Expected sympy.Symbol, got {orig_s}")
  3605. if not isinstance(new_s, sympy.Symbol):
  3606. raise AssertionError(f"Expected sympy.Symbol, got {new_s}")
  3607. if not free_unbacked_symbols(new_s):
  3608. raise AssertionError(
  3609. f"Expected new_s to have free unbacked symbols: {new_s}"
  3610. )
  3611. if not free_unbacked_symbols(orig_s):
  3612. raise AssertionError(
  3613. f"Expected orig_s to have free unbacked symbols: {orig_s}"
  3614. )
  3615. dest = self.replacements.get(orig_s)
  3616. if dest is not None:
  3617. if free_unbacked_symbols(dest):
  3618. raise AssertionError(f"{orig_s} -> {dest}")
  3619. self._set_replacement(orig_s, new_s, "rename_unbacked_to")
  3620. self.unbacked_renamings[orig_s] = new_s
  3621. if dest is not None:
  3622. self._set_replacement(new_s, dest, "rename_unbacked_to_dest")
  3623. @record_shapeenv_event()
  3624. def _constrain_is_bounded(self, a: sympy.Symbol, upper_bound: int) -> None:
  3625. # TODO: Do something nontrivial when upper_bound is expression
  3626. pass
  3627. @record_shapeenv_event()
  3628. def _constrain_range_for_size(
  3629. self, a: sympy.Symbol, min: Optional[int] = None, max: Optional[int] = None
  3630. ) -> None:
  3631. if min is None:
  3632. min = 0
  3633. if max is None:
  3634. max = int_oo
  3635. if max < min:
  3636. raise ValueError(
  3637. "Maximum value to constrain_as_size can't be less than the specified min value, "
  3638. f"received min={min} and max={max}"
  3639. )
  3640. self.constrain_symbol_range(
  3641. a,
  3642. compiler_min=min,
  3643. compiler_max=max,
  3644. )
  3645. self.size_like.add(a)
  3646. @record_shapeenv_event()
  3647. def _constrain_range(self, a: sympy.Expr, min: int, max: int) -> None:
  3648. if isinstance(a, sympy.Integer):
  3649. if not (min <= int(a) <= max):
  3650. raise ValueRangeError(f"Invalid value {int(a)} for range [{min}:{max}]")
  3651. return
  3652. # TODO: Shouldn't we install a guard if the symbol is backed? Or is the
  3653. # semantics that this is an "unchecked" assert (but it this actually
  3654. # something useful? Might be better to restrict only for unbacked
  3655. # SymInt).
  3656. if isinstance(a, sympy.Symbol):
  3657. self.constrain_symbol_range(
  3658. a,
  3659. compiler_min=min,
  3660. compiler_max=max,
  3661. )
  3662. @record_shapeenv_event()
  3663. def _constrain_unify(self, a: SymInt, b: SymInt) -> None:
  3664. """
  3665. Given two SymInts, constrain them so that they must be equal. NB:
  3666. this will not work with SymInts that represent nontrivial expressions
  3667. (yet!)
  3668. """
  3669. # TODO: this does not install a deferred runtime assert yet
  3670. # TODO: Maybe dedupe this with _maybe_guard_rel?
  3671. # Update Feb 2024: this is extra important to do, this doesn't handle
  3672. # unbacked replacements properly nor does it generate deferred runtime
  3673. # asserts
  3674. if not isinstance(a, SymInt):
  3675. if not isinstance(b, SymInt):
  3676. if a != b:
  3677. raise AssertionError(f"Expected {a} == {b}")
  3678. else:
  3679. if not isinstance(b.node.expr, sympy.Symbol):
  3680. raise AssertionError("constraining non-Symbols NYI")
  3681. if b.node.shape_env is not self:
  3682. raise AssertionError("b.node.shape_env must be self")
  3683. self.replacements[b.node.expr] = sympy.Integer(a)
  3684. else:
  3685. # TODO: Actually, we can support this as long as one of them is a symbol.
  3686. # NB: We can't actually do "unification" as our operators are not
  3687. # injective
  3688. if not isinstance(a.node.expr, sympy.Symbol):
  3689. raise AssertionError("constraining non-Symbols NYI")
  3690. if a.node.shape_env is not self:
  3691. raise AssertionError("a.node.shape_env must be self")
  3692. if not isinstance(b, SymInt):
  3693. self.replacements[a.node.expr] = sympy.Integer(b)
  3694. else:
  3695. if a.node.shape_env is not b.node.shape_env:
  3696. raise AssertionError("a.node.shape_env must be b.node.shape_env")
  3697. if not isinstance(b.node.expr, sympy.Symbol):
  3698. raise AssertionError("constraining non-Symbols NYI")
  3699. new_var = self._find(a.node.expr)
  3700. self.replacements[b.node.expr] = new_var
  3701. def _ignore_fresh_unbacked_symbols_tls(self) -> bool:
  3702. return getattr(TLS, "ignore_fresh_unbacked_symbols", False)
  3703. @record_shapeenv_event()
  3704. def _ignore_fresh_unbacked_symbols_set(self, b: bool) -> bool:
  3705. prev = self._ignore_fresh_unbacked_symbols_tls()
  3706. TLS.ignore_fresh_unbacked_symbols = b
  3707. return prev
  3708. @contextmanager
  3709. def ignore_fresh_unbacked_symbols(self) -> Iterator[None]:
  3710. """
  3711. Indicates that the newly allocated unbacked SymInts are being
  3712. discarded
  3713. """
  3714. prev = self._ignore_fresh_unbacked_symbols_set(True)
  3715. try:
  3716. yield
  3717. finally:
  3718. self._ignore_fresh_unbacked_symbols_set(prev)
  3719. @record_shapeenv_event()
  3720. def freeze(self) -> None:
  3721. """Freeze this ShapeEnv to stop accumulating guards
  3722. A frozen ShapeEnv will ignore any further guards generated on it and
  3723. only emit a warning which may lead to accuracy problems.
  3724. """
  3725. self.frozen = True
  3726. @record_shapeenv_event()
  3727. def freeze_runtime_asserts(self) -> None:
  3728. """Freeze this ShapeEnv to stop adding deferred runtime asserts.
  3729. We will error if you try to install a new runtime assert when it is
  3730. frozen. This would indicate a lowering violation, or perhaps something
  3731. we know statically is already True but we are checking it again in a way
  3732. that is not clearly dischargeable.
  3733. """
  3734. # self.prefer_deferred_runtime_asserts_over_guards = False
  3735. self.runtime_asserts_frozen = True
  3736. def _create_symbol_for_source(self, source: Source) -> Optional[sympy.Symbol]:
  3737. if not self._translation_validation_enabled:
  3738. return None
  3739. srcname = source.name
  3740. if source not in self.source_to_symbol:
  3741. self.source_to_symbol[srcname] = sympy.Symbol(srcname, integer=True)
  3742. return self.source_to_symbol[srcname]
  3743. def _add_z3var(self, symbol: sympy.Symbol, type: type) -> None:
  3744. if self._translation_validation_enabled:
  3745. self.validator.add_var(symbol, type)
  3746. def _add_target_expr(self, expr: SympyBoolean) -> None:
  3747. if self._translation_validation_enabled:
  3748. self.validator.add_target_expr(expr)
  3749. def _add_assertion(self, expr: SympyBoolean) -> None:
  3750. if self._translation_validation_enabled:
  3751. self.validator.add_assertion(expr)
  3752. def _check_translation_validate(self) -> None:
  3753. if self._translation_validation_enabled:
  3754. self.validator.validate()
  3755. @record_shapeenv_event()
  3756. def _create_fx_call_function(
  3757. self,
  3758. op: Callable,
  3759. args: tuple,
  3760. ) -> tuple[Optional[torch.fx.Node], bool]:
  3761. # Cache this tuple in order to avoid duplicated nodes.
  3762. node_key = (op, args)
  3763. # Flags whether the returned node was cached or not.
  3764. fresh = False
  3765. if self._translation_validation_enabled and node_key not in self.fx_node_cache:
  3766. # Presence of None in the arguments implies that we should ignore this operation.
  3767. if any(a is None for a in args):
  3768. # We check if we are not mixing SymNode that should not be ignored
  3769. # (fx_node is not None) with those that should (fx_node is None).
  3770. if not all(not isinstance(a, torch.fx.Node) for a in args):
  3771. raise AssertionError(
  3772. "Cannot mix SymNodes with fx_node and without fx_node"
  3773. )
  3774. return None, fresh
  3775. fresh = True
  3776. # If translation validation is enabled, all arguments must have its
  3777. # own FX node.
  3778. if not all(a is not None for a in args):
  3779. raise AssertionError(f"missing arg in FX graph ({op.__name__}): {args}")
  3780. node = self.fx_node_cache[node_key] = self.graph.call_function(op, args)
  3781. self.name_to_node[node.name] = node
  3782. return self.fx_node_cache.get(node_key, None), fresh
  3783. def _create_fx_placeholder_and_z3var(
  3784. self,
  3785. symbol: sympy.Symbol,
  3786. type: type,
  3787. ) -> Optional[torch.fx.Node]:
  3788. if not self._translation_validation_enabled:
  3789. return None
  3790. node_key = (self.graph.placeholder, (symbol,))
  3791. # Check if we haven't added this symbol already.
  3792. # If so, skip the placeholder creation, as it
  3793. # generates invalid Python code.
  3794. if node_key not in self.fx_node_cache:
  3795. # Add a Z3 variable according to 'type'.
  3796. self._add_z3var(symbol, type)
  3797. # Create the FX placeholder out of a mangled name.
  3798. mangled_name = re.sub(
  3799. r"[^a-zA-Z0-9]", "_", re.sub(r"[()]", "", symbol.name)
  3800. )
  3801. node = self.fx_node_cache[node_key] = self.graph.placeholder(mangled_name)
  3802. self.name_to_node[node.name] = node
  3803. # Attach the 'symbol' to the placeholder so that we can retrieve
  3804. # the Z3 variable later.
  3805. node.meta["symbol"] = symbol
  3806. return self.fx_node_cache[node_key]
  3807. def _remove_fx_node(self, node: Optional[torch.fx.Node]) -> None:
  3808. if self._translation_validation_enabled and node is not None:
  3809. self.name_to_node.pop(node.name)
  3810. self.graph.erase_node(node)
  3811. def _add_fx_node_metadata(self, node: torch.fx.Node) -> None:
  3812. from torch._dynamo.utils import get_current_node
  3813. if self.should_record_events:
  3814. node.meta[SHAPEENV_EVENT_KEY] = self._last_event_index()
  3815. node.meta[CURRENT_NODE_KEY] = get_current_node()
  3816. @staticmethod
  3817. def _suppress_guards_tls() -> bool:
  3818. return getattr(TLS, "suppress_guards", False)
  3819. @record_shapeenv_event()
  3820. def _suppress_guards_enter(self) -> None:
  3821. if not hasattr(TLS, "suppress_guards_stack"):
  3822. TLS.suppress_guards_stack = []
  3823. old = self._suppress_guards_tls()
  3824. TLS.suppress_guards_stack.append(old)
  3825. TLS.suppress_guards = True
  3826. @record_shapeenv_event()
  3827. def _suppress_guards_exit(self) -> None:
  3828. old = (
  3829. TLS.suppress_guards_stack.pop()
  3830. if len(TLS.suppress_guards_stack) > 0
  3831. else False
  3832. )
  3833. TLS.suppress_guards = old
  3834. def suppress_guards(self) -> _GeneratorContextManager[None]:
  3835. """Context manager to ignore all guards generated inside"""
  3836. return _suppress_guards(self)
  3837. def _get_key(self) -> tuple[int, int, int, int]:
  3838. """
  3839. Defines the current "state" of the guards we've accumulated in this ShapeEnv.
  3840. Determines when we need to invalidate our cache
  3841. """
  3842. return (
  3843. len(self.replacements),
  3844. len(self.divisible),
  3845. self.num_deferred_runtime_asserts,
  3846. len(self.real_tensor_prop_unbacked_vals),
  3847. )
  3848. def _update_version_counter(self) -> None:
  3849. # if the change to shape env effects self.divisible set
  3850. # _resimplify_floor_div_axioms.
  3851. # This is used to trigger a resimplication of FloorDiv to CleanDivs
  3852. # in implication inside the function resimplify_floor_div.
  3853. if len(self.divisible) != self._prev_cache_key[1]:
  3854. self._resimplify_floor_div_axioms = True
  3855. # The shape environment is queried orders of magnitude more often than
  3856. # it is changed, so we summarise the cache key into a linearly
  3857. # increasing version counter which is cheaper to check in _lru_cache
  3858. # Only update version counter if the state actually changed
  3859. cur_key = self._get_key()
  3860. if self._prev_cache_key != cur_key:
  3861. self._prev_cache_key = cur_key
  3862. self._version_counter += 1
  3863. def _produce_dyn_sizes(
  3864. self,
  3865. ex_size: Sequence[IntLikeType],
  3866. source: Source,
  3867. symbolic_context: SymbolicContext,
  3868. ) -> list[sympy.Expr]:
  3869. return self._produce_dyn_sizes_from_int_tuple(
  3870. tuple(ex_size), source, symbolic_context
  3871. )
  3872. def _produce_dyn_sizes_from_int_tuple(
  3873. self,
  3874. tensor_size: Sequence[IntLikeType],
  3875. source: Source,
  3876. symbolic_context: SymbolicContext,
  3877. hint_overrides: Optional[dict[int, int]] = None,
  3878. ) -> list[sympy.Expr]:
  3879. if not all(not is_symbolic(val) for val in tensor_size):
  3880. raise AssertionError(
  3881. f"Expect size to be a plain tuple of ints but got {tensor_size}"
  3882. )
  3883. from torch._dynamo.source import TensorProperty, TensorPropertySource
  3884. if not hint_overrides:
  3885. hint_overrides = {}
  3886. _assert_symbol_context(symbolic_context)
  3887. dynamic_dims = symbolic_context.dynamic_sizes # type: ignore[attr-defined]
  3888. constraint_dims = symbolic_context.constraint_sizes # type: ignore[attr-defined]
  3889. size = []
  3890. for i, val in enumerate(tensor_size):
  3891. sym = self.create_symbol(
  3892. hint_overrides.get(i, val),
  3893. TensorPropertySource(source, TensorProperty.SIZE, i),
  3894. dynamic_dims[i],
  3895. constraint_dims[i],
  3896. do_not_specialize_zero_one=config.backed_size_oblivious,
  3897. symbolic_context=symbolic_context,
  3898. )
  3899. if (
  3900. isinstance(symbolic_context, StatelessSymbolicContext)
  3901. and symbolic_context.specialize_on
  3902. ):
  3903. for specialization in symbolic_context.specialize_on[i]:
  3904. self.specializations.add(
  3905. Specialization(
  3906. TensorPropertySource(source, TensorProperty.SIZE, i),
  3907. specialization,
  3908. )
  3909. )
  3910. if (
  3911. config.backed_size_oblivious
  3912. and isinstance(sym, sympy.Symbol) # could be static
  3913. and symbol_is_type(sym, SymT.SIZE)
  3914. ):
  3915. self.size_like.add(sym)
  3916. size.append(sym)
  3917. return size
  3918. def create_symbolic_sizes_strides_storage_offset(
  3919. self,
  3920. ex: torch.Tensor,
  3921. source: Source,
  3922. *,
  3923. symbolic_context: Optional[SymbolicContext] = None,
  3924. ) -> tuple[
  3925. tuple[IntLikeType, ...],
  3926. tuple[IntLikeType, ...],
  3927. IntLikeType,
  3928. ]:
  3929. """
  3930. Returns a list of symbolic sizes and strides for the given tensor.
  3931. We try our best to express stride in terms of the sizes, so as to not
  3932. introduce new symbolic variables.
  3933. """
  3934. ex_size = tuple(
  3935. self._maybe_specialize_sym_int_with_hint(sz) for sz in ex.size()
  3936. )
  3937. ex_stride = tuple(
  3938. self._maybe_specialize_sym_int_with_hint(sd) for sd in ex.stride()
  3939. )
  3940. ex_storage_offset = self._maybe_specialize_sym_int_with_hint(
  3941. ex.storage_offset()
  3942. )
  3943. return self._create_symbolic_sizes_strides_storage_offset(
  3944. ex_size,
  3945. ex_stride,
  3946. ex_storage_offset,
  3947. [_is_dim_dynamic(ex, i) for i in range(ex.dim())],
  3948. source,
  3949. symbolic_context=symbolic_context,
  3950. )
  3951. # Dynamo may want to wrap FakeTensors with SymInt sizes up e.g. make_fx(opt_f(), tracing_mode="symbolic").
  3952. # We create symbols in shape_env using the backed hints behind SymInt.
  3953. # Case 1: when SymInt is backed, dynamo can proceed with FakeTensors that have concrete shape.
  3954. # produce_guards will trigger specializations on the outer stuff
  3955. # Case 2: when the SymInt is unbacked, we will throw an data dependent error in require_hint().
  3956. #
  3957. # It's probably good for now but it's important to note that this approach has implications for
  3958. # the original shape_env when checking guards in different order.
  3959. # Example:
  3960. # ---------
  3961. # Consider a function "opt_f" as shown below:
  3962. # @torch.compile()
  3963. # def opt_f(x: bool, y: Tensor):
  3964. # if x == True:
  3965. # return y + torch.randn([4])
  3966. # else:
  3967. # return y
  3968. # Depending on the sequence of calls, we might install two different sets of guards:
  3969. # 1. opt_f(False, y):
  3970. # - "x == False" (always works for any size y)
  3971. # 2. opt_f(True, y):
  3972. # - Triggers recompilation and results in guards like:
  3973. # - "x == True and y.size(0) == 4"
  3974. # - (or "y.size(0) == 4 and x == True")
  3975. # The order of checking the guards matters. In this specific example:
  3976. # If True branch guard check precedes False branch and for True branch, y.size(0) check precedes x == True,
  3977. # we may have an unnecessary shape specialization for y.
  3978. def _maybe_specialize_sym_int_with_hint(
  3979. self, maybe_sym: IntLikeType
  3980. ) -> IntLikeType:
  3981. if not isinstance(maybe_sym, (int, torch.SymInt)):
  3982. raise AssertionError(f"Expected int or SymInt, got {type(maybe_sym)}")
  3983. if is_symbolic(maybe_sym):
  3984. if maybe_sym.node.shape_env is self:
  3985. raise AssertionError(
  3986. "expect the symbol is created from an shape env other than current one."
  3987. )
  3988. return maybe_sym.node.require_hint()
  3989. return maybe_sym
  3990. @record_shapeenv_event()
  3991. def _create_symbolic_sizes_strides_storage_offset(
  3992. self,
  3993. # NB: SymInt is allowed here due to nested int, normally you don't
  3994. # actually pass true symbolic sizes to this function
  3995. ex_size: Sequence[IntLikeType],
  3996. ex_stride: Sequence[IntLikeType],
  3997. ex_storage_offset: IntLikeType,
  3998. is_dim_dynamic: Sequence[bool],
  3999. source: Source,
  4000. *,
  4001. symbolic_context: Optional[SymbolicContext] = None,
  4002. hint_overrides: Optional[dict[int, int]] = None,
  4003. ) -> tuple[
  4004. tuple[IntLikeType, ...],
  4005. tuple[IntLikeType, ...],
  4006. IntLikeType,
  4007. ]:
  4008. dim = len(ex_size)
  4009. if not hint_overrides:
  4010. hint_overrides = {}
  4011. # Reimplement the legacy behavior
  4012. if symbolic_context is None:
  4013. constraint_sizes: list[DimConstraint] = [None] * dim
  4014. constraint_strides: list[DimConstraint] = [None] * dim
  4015. dynamic_dims = []
  4016. dynamic_strides = []
  4017. for i in range(dim):
  4018. # NB: This is encapsulation breaking! Legacy behavior was
  4019. # bad.
  4020. if is_dim_dynamic[i]:
  4021. r = DimDynamic.DYNAMIC
  4022. elif self.assume_static_by_default:
  4023. r = DimDynamic.STATIC
  4024. else:
  4025. r = DimDynamic.DUCK
  4026. dynamic_dims.append(r)
  4027. dynamic_strides.append(r)
  4028. dynamic_dims = [DimDynamic.DUCK] * dim
  4029. dynamic_strides = [DimDynamic.INFER_STRIDE] * dim
  4030. # symbolic_context is None - set one
  4031. symbolic_context = StatelessSymbolicContext(
  4032. dynamic_sizes=dynamic_dims,
  4033. dynamic_strides=dynamic_strides,
  4034. constraint_sizes=constraint_sizes,
  4035. constraint_strides=constraint_strides,
  4036. )
  4037. # We got a StatelessSymbolicContext
  4038. _assert_symbol_context(symbolic_context)
  4039. constraint_sizes = symbolic_context.constraint_sizes # type: ignore[attr-defined]
  4040. constraint_strides = symbolic_context.constraint_strides # type: ignore[attr-defined]
  4041. dynamic_sizes = symbolic_context.dynamic_sizes # type: ignore[attr-defined]
  4042. dynamic_strides = symbolic_context.dynamic_strides # type: ignore[attr-defined]
  4043. # TODO: make this configurable from outside symbolic_context; we made a symbolic_context
  4044. # decision here where if all sizes are static, we are going to
  4045. # specialize all of the inner strides/offset too. We don't have to
  4046. # do this, and arguably we should ALWAYS allow for dynamic offset,
  4047. # this is cheap.
  4048. # TODO: This should be DYNAMIC, using DUCK for BC
  4049. dynamic_offset = (
  4050. DimDynamic.STATIC
  4051. if all(r == DimDynamic.STATIC for r in dynamic_sizes)
  4052. else DimDynamic.DUCK
  4053. )
  4054. are_sizes_static = all(r == DimDynamic.STATIC for r in dynamic_sizes)
  4055. if len(dynamic_sizes) != dim:
  4056. raise AssertionError(f"{len(dynamic_sizes)} != {dim}")
  4057. if len(dynamic_strides) != dim:
  4058. raise AssertionError(f"{len(dynamic_strides)} != {dim}")
  4059. if len(constraint_sizes) != dim:
  4060. raise AssertionError(f"len(constraint_sizes) != {dim}")
  4061. if len(constraint_strides) != dim:
  4062. raise AssertionError(f"len(constraint_strides) != {dim}")
  4063. from torch._dynamo.source import TensorProperty, TensorPropertySource
  4064. size: list[sympy.Expr] = self._produce_dyn_sizes_from_int_tuple(
  4065. ex_size, source, symbolic_context, hint_overrides=hint_overrides
  4066. )
  4067. stride = self._compute_symbolic_stride(
  4068. source,
  4069. size,
  4070. ex_size,
  4071. ex_stride,
  4072. dynamic_strides,
  4073. constraint_strides,
  4074. are_sizes_static,
  4075. symbolic_context,
  4076. )
  4077. sym_sizes = [
  4078. self.create_symintnode(
  4079. sym,
  4080. hint=hint_overrides.get(i, hint),
  4081. source=TensorPropertySource(source, TensorProperty.SIZE, i),
  4082. )
  4083. for i, (sym, hint) in enumerate(zip(size, ex_size))
  4084. ]
  4085. for i, sym in enumerate(sym_sizes):
  4086. if isinstance(sym, torch.SymInt) and i in hint_overrides:
  4087. self.var_to_hint_override[sym.node.expr] = hint_overrides[i]
  4088. sym_stride = []
  4089. for i, stride_expr in enumerate(stride):
  4090. # NB: Don't duck size the stride; instead use the expression
  4091. # we computed
  4092. if stride_expr is None:
  4093. raise AssertionError(f"stride_expr is None for index {i}")
  4094. # self.backed_var_to_val will have the up to date hint value for each symbols
  4095. # including overridden hints.
  4096. hint_stride = stride_expr.xreplace(self.backed_var_to_val)
  4097. if isinstance(hint_stride, (int, sympy.core.numbers.Integer)):
  4098. hint_stride = int(hint_stride)
  4099. else:
  4100. hint_stride = ex_stride[i]
  4101. sym_stride.append(
  4102. self.create_symintnode(
  4103. stride_expr,
  4104. hint=hint_stride,
  4105. source=TensorPropertySource(source, TensorProperty.STRIDE, i),
  4106. )
  4107. )
  4108. sym_storage_offset = self.create_symintnode(
  4109. self.create_symbol(
  4110. ex_storage_offset,
  4111. TensorPropertySource(source, TensorProperty.STORAGE_OFFSET),
  4112. dynamic_dim=dynamic_offset,
  4113. constraint_dim=None,
  4114. symbolic_context=symbolic_context,
  4115. ),
  4116. hint=ex_storage_offset,
  4117. source=TensorPropertySource(source, TensorProperty.STORAGE_OFFSET),
  4118. )
  4119. return tuple(sym_sizes), tuple(sym_stride), sym_storage_offset
  4120. def _compute_symbolic_stride(
  4121. self,
  4122. source: Source,
  4123. size: Sequence[sympy.Expr],
  4124. ex_size: Sequence[IntLikeType],
  4125. ex_stride: Sequence[IntLikeType],
  4126. dynamic_strides: Sequence[DimDynamic],
  4127. constraint_strides: Sequence[
  4128. Optional[Union[StrictMinMaxConstraint, RelaxedUnspecConstraint]]
  4129. ],
  4130. are_sizes_static: bool,
  4131. symbolic_context: SymbolicContext,
  4132. ) -> list[sympy.Expr]:
  4133. from torch._dynamo.source import TensorProperty, TensorPropertySource
  4134. stride: list[Optional[sympy.Expr]] = [None] * len(size)
  4135. candidates: dict[IntLikeType, sympy.Expr] = {}
  4136. # iterate over unbound strides in val ascending order with
  4137. # index descending as a tie breaker since for cases like
  4138. # [(1, 1), (1, 0)], we want to fill in the right most
  4139. # stride first.
  4140. val_list = [(val, -i) for i, val in enumerate(ex_stride)]
  4141. val_list.sort(key=_nested_int_aware_sort)
  4142. for val, neg_i in val_list:
  4143. i = -neg_i
  4144. contiguous_stride = (
  4145. i != len(ex_stride) - 1
  4146. and ex_stride[i] == ex_size[i + 1] * ex_stride[i + 1]
  4147. )
  4148. if val in (0, 1) and not contiguous_stride:
  4149. out_stride = sympy.Integer(val)
  4150. else:
  4151. dynamic_stride = dynamic_strides[i]
  4152. if dynamic_stride == DimDynamic.INFER_STRIDE and val in candidates:
  4153. # Set stride to a candidate only for DimDynamic.INFER_STRIDE
  4154. out_stride = candidates[val]
  4155. else:
  4156. # Set INFER_STRIDE to STATIC or DUCK depending on sizes
  4157. dyn_stride = dynamic_stride
  4158. if dynamic_stride == DimDynamic.INFER_STRIDE:
  4159. dyn_stride = (
  4160. DimDynamic.STATIC if are_sizes_static else DimDynamic.DUCK
  4161. )
  4162. out_stride = self.create_symbol(
  4163. val,
  4164. TensorPropertySource(source, TensorProperty.STRIDE, i),
  4165. dynamic_dim=dyn_stride,
  4166. constraint_dim=constraint_strides[i],
  4167. symbolic_context=symbolic_context,
  4168. )
  4169. stride[i] = out_stride
  4170. candidates[ex_size[i] * val] = size[i] * out_stride
  4171. if not all(x is not None for x in stride):
  4172. raise AssertionError("All stride elements must be non-None")
  4173. return stride
  4174. @record_shapeenv_event()
  4175. def create_symintnode(
  4176. self,
  4177. sym: sympy.Expr,
  4178. *,
  4179. hint: Optional[int],
  4180. source: Optional[Source] = None,
  4181. ) -> IntLikeType:
  4182. """Create a SymInt value from a symbolic expression
  4183. If you know what the current hint value of the SymInt to be created
  4184. is, pass it into hint. Otherwise, pass None and we will make our best
  4185. guess
  4186. """
  4187. if self._translation_validation_enabled and source is not None:
  4188. # Create a new symbol for this source.
  4189. symbol = self._create_symbol_for_source(source)
  4190. if symbol is None:
  4191. raise AssertionError("symbol must not be None")
  4192. # Create a new FX placeholder and Z3 variable for 'symbol'.
  4193. fx_node = self._create_fx_placeholder_and_z3var(symbol, int)
  4194. # Add an equality assertion for the newly created symbol and 'sym'.
  4195. self._add_assertion(sympy.Eq(symbol, sym))
  4196. else:
  4197. fx_node = None
  4198. out: IntLikeType
  4199. if isinstance(sym, sympy.Integer):
  4200. if hint is not None:
  4201. if int(sym) != hint:
  4202. raise AssertionError(f"int(sym)={int(sym)} != hint={hint}")
  4203. out = int(sym)
  4204. else:
  4205. # How can this occur? When we mark_unbacked, we end up with a real
  4206. # tensor that has hints for all sizes, but we MUST NOT create a
  4207. # SymNode with a hint, because we're hiding the hint from our eyes
  4208. # with the unbacked Symbol. And in fact, the hint compute may be
  4209. # inconsistent with size oblivious tests.
  4210. if free_unbacked_symbols(sym):
  4211. hint = None
  4212. out = SymInt(SymNode(sym, self, int, hint, fx_node=fx_node))
  4213. return out
  4214. @record_shapeenv_event()
  4215. def create_symfloatnode(
  4216. self,
  4217. sym: sympy.Expr,
  4218. *,
  4219. hint: Optional[int | float | bool],
  4220. source: Optional[Source] = None,
  4221. ) -> FloatLikeType:
  4222. """Create a SymFloat value from a symbolic expression"""
  4223. if self._translation_validation_enabled and source is not None:
  4224. # Create a new symbol for this source.
  4225. symbol = self._create_symbol_for_source(source)
  4226. if symbol is None:
  4227. raise AssertionError("symbol must not be None")
  4228. # Create a new FX placeholder and Z3 variable for 'symbol'.
  4229. fx_node = self._create_fx_placeholder_and_z3var(symbol, float)
  4230. # Add an equality assertion for the newly created symbol and 'sym'.
  4231. self._add_assertion(sympy.Eq(symbol, sym))
  4232. else:
  4233. fx_node = None
  4234. out: FloatLikeType
  4235. if isinstance(sym, sympy.Float):
  4236. if hint is not None:
  4237. if float(sym) != hint:
  4238. raise AssertionError(f"float(sym)={float(sym)} != hint={hint}")
  4239. out = float(sym)
  4240. else:
  4241. # You could give this the same treatment as SymInt above if
  4242. # you supported mark_unbacked on a float, but it's a kind of
  4243. # strange thing to do though because floats don't get 0/1
  4244. # specialization anyway
  4245. if free_unbacked_symbols(sym):
  4246. if hint is not None:
  4247. raise AssertionError(
  4248. f"hint must be None for unbacked symbol: {sym}"
  4249. )
  4250. out = SymFloat(SymNode(sym, self, float, hint, fx_node=fx_node))
  4251. return out
  4252. @record_shapeenv_event()
  4253. def create_unspecified_symint_and_symbol(
  4254. self, value: int, source: Source, dynamic_dim: DimDynamic
  4255. ) -> IntLikeType:
  4256. """Create a SymInt wrapping a new unspecified symbol"""
  4257. return self.create_symintnode(
  4258. self.create_unspecified_symbol(
  4259. value,
  4260. source=source,
  4261. dynamic_dim=dynamic_dim,
  4262. ),
  4263. hint=value,
  4264. source=source,
  4265. )
  4266. def create_symboolnode(self, sym: sympy.Expr) -> SymBool:
  4267. """Create a SymBool object from a sympy boolean expression"""
  4268. # This function is only being used in serialization, so we do not track it
  4269. # for validation.
  4270. return SymBool(SymNode(sym, self, bool, None))
  4271. def _log_create_unbacked_symbol(
  4272. self,
  4273. prefix: str,
  4274. symbol: sympy.Symbol,
  4275. vr: ValueRanges,
  4276. source: Optional[Source] = None,
  4277. sym_node: Optional[SymNode] = None,
  4278. ) -> None:
  4279. is_debug = config.extended_debug_create_symbol is not None and str(
  4280. symbol
  4281. ) in config.extended_debug_create_symbol.split(",")
  4282. sloc: Union[str, SLoc]
  4283. if source is None:
  4284. sloc, maybe_extra_debug = self._get_stack_summary(is_debug)
  4285. else:
  4286. sloc, maybe_extra_debug = source.name, ""
  4287. log.info(
  4288. "%s %s [%s, %s] %s%s",
  4289. prefix,
  4290. symbol,
  4291. vr.lower,
  4292. vr.upper,
  4293. sloc,
  4294. maybe_extra_debug,
  4295. stack_info=is_debug,
  4296. )
  4297. trace_structured(
  4298. "create_unbacked_symbol",
  4299. metadata_fn=lambda: {
  4300. "symbol": str(symbol),
  4301. "node_id": id(sym_node),
  4302. "vr": f"[{vr.lower}, {vr.upper}]",
  4303. "user_stack": structured.get_user_stack(3),
  4304. "stack": structured.get_framework_stack(),
  4305. },
  4306. )
  4307. @record_shapeenv_event()
  4308. def create_unbacked_symfloat(self) -> SymFloat:
  4309. """Create a symbolic float without a hint value"""
  4310. symbol: sympy.Symbol = make_symbol(
  4311. SymT.UNBACKED_FLOAT, self.unbacked_symfloat_counter
  4312. )
  4313. self.unbacked_symfloat_counter += 1
  4314. self.counter["create_unbacked_symbol"] += 1
  4315. if not self._ignore_fresh_unbacked_symbols_tls():
  4316. self.pending_fresh_unbacked_symbols.append(symbol)
  4317. self.var_to_stack[symbol] = CapturedTraceback.extract(skip=1)
  4318. vr = self.var_to_range[symbol] = ValueRanges.unknown()
  4319. if not vr.is_float:
  4320. raise AssertionError("vr must be float")
  4321. sloc = self._get_sloc()
  4322. self.var_to_range_sloc[symbol] = ValueRangesSLoc(sloc, sloc)
  4323. # Create a new FX placeholder and Z3 variable for 'symbol'.
  4324. fx_node = self._create_fx_placeholder_and_z3var(symbol, float)
  4325. sym_node = SymNode(symbol, self, float, None, fx_node=fx_node)
  4326. self._log_create_unbacked_symbol(
  4327. "create_unbacked_symfloat", symbol, vr, sym_node=sym_node
  4328. )
  4329. return SymFloat(sym_node)
  4330. @record_shapeenv_event()
  4331. def create_unbacked_symint(self, source: Optional[Source] = None) -> SymInt:
  4332. """Create a symbolic integer without a hint value"""
  4333. symbol: sympy.Symbol = make_symbol(
  4334. SymT.UNBACKED_INT, self.unbacked_symint_counter, integer=True
  4335. )
  4336. self.unbacked_symint_counter += 1
  4337. if not self._ignore_fresh_unbacked_symbols_tls():
  4338. self.pending_fresh_unbacked_symbols.append(symbol)
  4339. self.counter["create_unbacked_symbol"] += 1
  4340. self.var_to_stack[symbol] = CapturedTraceback.extract(skip=1)
  4341. vr = self.var_to_range[symbol] = self._default_unspecified_value_range()
  4342. if not vr.is_int:
  4343. raise AssertionError("vr must be int")
  4344. sloc = self._get_sloc()
  4345. self.var_to_range_sloc[symbol] = ValueRangesSLoc(sloc, sloc)
  4346. # Create a new FX placeholder and Z3 variable for 'symbol'.
  4347. fx_node = self._create_fx_placeholder_and_z3var(symbol, int)
  4348. sym_node = SymNode(symbol, self, int, None, fx_node=fx_node)
  4349. self._log_create_unbacked_symbol(
  4350. "create_unbacked_symint", symbol, vr, source, sym_node=sym_node
  4351. )
  4352. return SymInt(sym_node)
  4353. def is_unbacked_symint(self, symbol: sympy.Symbol) -> bool:
  4354. """Check if a sympy symbol matches the naming convention for unbacked symbols"""
  4355. return symbol_is_type(symbol, SymT.UNBACKED_INT)
  4356. @record_shapeenv_event()
  4357. def create_unbacked_symbool(self) -> SymBool:
  4358. """Create a symbolic boolean without a hint value"""
  4359. symbol: sympy.Symbol = make_symbol(
  4360. SymT.UNBACKED_INT, self.unbacked_symint_counter, integer=True
  4361. )
  4362. self.unbacked_symint_counter += 1
  4363. if not self._ignore_fresh_unbacked_symbols_tls():
  4364. self.pending_fresh_unbacked_symbols.append(symbol)
  4365. self.counter["create_unbacked_symbol"] += 1
  4366. self.var_to_stack[symbol] = CapturedTraceback.extract(skip=1)
  4367. vr = self.var_to_range[symbol] = ValueRanges(0, 1)
  4368. if not vr.is_int:
  4369. raise AssertionError("vr must be int")
  4370. sloc = self._get_sloc("default value range for unbacked SymBool")
  4371. self.var_to_range_sloc[symbol] = ValueRangesSLoc(sloc, sloc)
  4372. # Create a new FX placeholder and Z3 variable for 'symbol'.
  4373. fx_node = self._create_fx_placeholder_and_z3var(symbol, bool)
  4374. sym_node = SymNode(sympy.Eq(symbol, 1), self, bool, None, fx_node=fx_node)
  4375. self._log_create_unbacked_symbol(
  4376. "create_unbacked_symbool", symbol, vr, sym_node=sym_node
  4377. )
  4378. return SymBool(sym_node)
  4379. @record_shapeenv_event()
  4380. def create_unspecified_symbol(
  4381. self,
  4382. val: Union[int, SymInt, float, SymFloat],
  4383. source: Source,
  4384. dynamic_dim: DimDynamic = DimDynamic.DUCK,
  4385. constraint_dim: DimConstraint = None, # NB: includes None
  4386. symbolic_context: Optional[StatelessSymbolicContext] = None,
  4387. ) -> sympy.Expr:
  4388. """
  4389. Create a symbol with an unspecified value
  4390. Compared to standard symbols we do not assume the value is positive,
  4391. nor do we specialze on zero or one values.
  4392. """
  4393. # 'positive' is None for unspecified symbols, since we can't
  4394. # assume that it will be neither positive nor negative.
  4395. # We don't want to specialize zero one val for unspecified symbol
  4396. # so that we can always get a new symbol despite val.
  4397. return self.create_symbol(
  4398. val,
  4399. source,
  4400. dynamic_dim,
  4401. constraint_dim,
  4402. positive=None,
  4403. do_not_specialize_zero_one=True,
  4404. symbolic_context=symbolic_context,
  4405. )
  4406. @record_shapeenv_event()
  4407. def create_symbol(
  4408. self,
  4409. val: int,
  4410. source: Source,
  4411. dynamic_dim: DimDynamic = DimDynamic.DUCK,
  4412. constraint_dim: DimConstraint = None, # NB: includes None
  4413. positive: Optional[bool] = True,
  4414. do_not_specialize_zero_one: bool = False,
  4415. symbolic_context: Optional[StatelessSymbolicContext] = None,
  4416. ) -> sympy.Expr:
  4417. """Create a new symbol which is tracked by this ShapeEnv"""
  4418. # check if constraint_dim is actually static integer
  4419. if (
  4420. isinstance(constraint_dim, StrictMinMaxConstraint)
  4421. and constraint_dim.vr.lower == constraint_dim.vr.upper
  4422. ):
  4423. dynamic_dim = DimDynamic.STATIC
  4424. if constraint_dim.vr.lower != val:
  4425. raise ConstraintViolationError(
  4426. f"Static shape constraint of {constraint_dim.vr.lower} does not match input size of {val}, "
  4427. f"for {source.name}"
  4428. )
  4429. if symbolic_context:
  4430. from torch._dynamo.source import TensorPropertySource
  4431. if not isinstance(source, TensorPropertySource):
  4432. raise AssertionError(
  4433. f"Expected TensorPropertySource, got {type(source)}"
  4434. )
  4435. # TODO: storage_offset handling?
  4436. if source.idx is None:
  4437. raise AssertionError("source.idx must not be None")
  4438. symbolic_context.dynamic_sizes[source.idx] = dynamic_dim
  4439. symbolic_context.constraint_sizes[source.idx] = None
  4440. constraint_dim = None
  4441. # see note [Tensor Fakification and Symbol Caching]
  4442. source_name = source.name
  4443. if (
  4444. isinstance(symbolic_context, StatefulSymbolicContext)
  4445. and id(self) not in symbolic_context.shape_env_to_source_to_symbol_cache
  4446. ):
  4447. symbolic_context.shape_env_to_source_to_symbol_cache[id(self)] = {}
  4448. if (
  4449. isinstance(symbolic_context, StatefulSymbolicContext)
  4450. and source_name
  4451. and (
  4452. source_name
  4453. in symbolic_context.shape_env_to_source_to_symbol_cache[id(self)]
  4454. )
  4455. ):
  4456. return symbolic_context.shape_env_to_source_to_symbol_cache[id(self)][
  4457. source_name
  4458. ]
  4459. if dynamic_dim is DimDynamic.UNBACKED:
  4460. # Check if this unbacked dimension has a shape_id.
  4461. # If so, we allocate a fresh symbol but add a runtime equality check
  4462. # via torch._check against the existing symbols with the same shape_id.
  4463. shape_id = None
  4464. if (
  4465. isinstance(symbolic_context, StatelessSymbolicContext)
  4466. and symbolic_context.shape_ids is not None
  4467. ):
  4468. from torch._dynamo.source import TensorPropertySource
  4469. if isinstance(source, TensorPropertySource) and source.idx is not None:
  4470. shape_id = symbolic_context.shape_ids.get(source.idx)
  4471. # Always allocate a fresh unbacked symbol
  4472. out = self.create_unbacked_symint(source).node.expr
  4473. self._constrain_range_for_size(out)
  4474. # Add runtime equality check for shape_id if applicable
  4475. if shape_id is not None:
  4476. if shape_id in self._shape_id_to_unbacked_symbol:
  4477. # Add runtime equality check instead of reusing the same symbol
  4478. existing_sym = self._shape_id_to_unbacked_symbol[shape_id]
  4479. existing_symint = self.create_symintnode(existing_sym, hint=None)
  4480. out_symint = self.create_symintnode(out, hint=None)
  4481. torch._check(out_symint == existing_symint)
  4482. else:
  4483. self._shape_id_to_unbacked_symbol[shape_id] = out
  4484. self.unbacked_inputs.add(out)
  4485. if isinstance(symbolic_context, StatefulSymbolicContext) and source_name:
  4486. symbolic_context.shape_env_to_source_to_symbol_cache[id(self)][
  4487. source_name
  4488. ] = out
  4489. return out
  4490. if do_not_specialize_zero_one:
  4491. specialize_zero_one = False
  4492. else:
  4493. specialize_zero_one = self.specialize_zero_one
  4494. if not isinstance(source, Source):
  4495. raise AssertionError(f"{type(source)} {source}")
  4496. if positive and val < 0:
  4497. raise AssertionError(f"positive set for negative value: {val}")
  4498. # It's always sound to allocate a symbol as DYNAMIC. If the user
  4499. # constrained the symbol, force the symbolic_context to DYNAMIC, because our
  4500. # constraint code will do weird stuff if, e.g., it's duck shaped
  4501. if constraint_dim is not None:
  4502. dynamic_dim = DimDynamic.DYNAMIC
  4503. if dynamic_dim is DimDynamic.STATIC:
  4504. out = sympy.Integer(val)
  4505. if isinstance(symbolic_context, StatefulSymbolicContext) and source_name:
  4506. symbolic_context.shape_env_to_source_to_symbol_cache[id(self)][
  4507. source_name
  4508. ] = out
  4509. return out
  4510. elif dynamic_dim is DimDynamic.DUCK:
  4511. # duck_shape can be used to globally turn off duck shaping, even
  4512. # if it was requested
  4513. duck = self.duck_shape
  4514. elif dynamic_dim is DimDynamic.DYNAMIC:
  4515. duck = False
  4516. else:
  4517. raise AssertionError(f"unhandled dynamic_dim {dynamic_dim}")
  4518. sloc = self._get_sloc()
  4519. if val in (0, 1) and specialize_zero_one:
  4520. if val == 0:
  4521. return sympy.S.Zero
  4522. else:
  4523. return sympy.S.One
  4524. elif not duck or val not in self.val_to_var:
  4525. # If we're not duck shaping, we always create a new symbol
  4526. # Even if we're duck shaping, if we haven't seen this particular
  4527. # value before, we also create a new symbol
  4528. symbol_id = self._generate_unique_id(source.name)
  4529. if type(val) is int or is_nested_int(val):
  4530. sympy_expr = make_symbol(
  4531. SymT.SIZE, symbol_id, positive=positive, integer=True
  4532. )
  4533. else:
  4534. sympy_expr = make_symbol(
  4535. SymT.FLOAT, symbol_id, positive=positive, real=True
  4536. )
  4537. self.source_to_var[source_name] = sympy_expr
  4538. # We always associate vars to vals
  4539. if isinstance(val, int):
  4540. self.backed_var_to_val[sympy_expr] = sympy.Integer(val)
  4541. elif isinstance(val, float):
  4542. self.backed_var_to_val[sympy_expr] = sympy.Float(val)
  4543. else:
  4544. # Only used for jagged layout nested tensors
  4545. self.backed_var_to_val[sympy_expr] = SingletonInt(
  4546. val.node.nested_int(), coeff=val.node.nested_int_coeff()
  4547. )
  4548. # Do the appending later, because we always want to populate this
  4549. self.var_to_sources[sympy_expr] = []
  4550. # Create a Z3 variable for the new symbol.
  4551. self._add_z3var(sympy_expr, int)
  4552. if duck:
  4553. # Make sure to reuse this symbol for subsequent duck shaping
  4554. self.val_to_var[val] = sympy_expr
  4555. if isinstance(val, int):
  4556. if positive:
  4557. # Add assertions for the newly created symbols
  4558. self._add_assertion(sympy_expr > 1)
  4559. # Apply default range, which assumes not zero-one
  4560. self.var_to_range[sympy_expr] = self._default_value_range(
  4561. do_not_specialize_zero_one
  4562. )
  4563. self.var_to_range_sloc[sympy_expr] = ValueRangesSLoc(
  4564. self._get_sloc(
  4565. "user code shown is first use of this value--the guard itself is not "
  4566. "due user code but due to 0/1 specialization in the framework; to "
  4567. "avoid specialization try torch._dynamo.decorators.mark_unbacked(tensor, dim)"
  4568. if self.specialize_zero_one
  4569. else None
  4570. ),
  4571. sloc,
  4572. )
  4573. else:
  4574. self.var_to_range[sympy_expr] = (
  4575. self._default_unspecified_value_range()
  4576. )
  4577. self.var_to_range_sloc[sympy_expr] = ValueRangesSLoc(sloc, sloc)
  4578. # Small performance optimization: if we have a min-max constraint,
  4579. # we can proactively narrow to that range
  4580. if isinstance(constraint_dim, StrictMinMaxConstraint):
  4581. if duck:
  4582. raise AssertionError(
  4583. "duck must be False for StrictMinMaxConstraint"
  4584. )
  4585. self._update_var_to_range(
  4586. sympy_expr, constraint_dim.vr, is_constraint=True
  4587. )
  4588. vr = self.var_to_range[sympy_expr]
  4589. if not vr.is_int:
  4590. raise AssertionError("vr must be int")
  4591. if val not in vr:
  4592. raise ConstraintViolationError(
  4593. f"{val} not in range [{vr.lower}, {vr.upper}]"
  4594. )
  4595. range_str = f"[{vr.lower}, {vr.upper}]"
  4596. elif isinstance(val, float):
  4597. self.var_to_range[sympy_expr] = vr = ValueRanges(-sympy.oo, sympy.oo)
  4598. self.var_to_range_sloc[sympy_expr] = ValueRangesSLoc(sloc, sloc)
  4599. range_str = f"[{vr.lower}, {vr.upper}]"
  4600. if not vr.is_float:
  4601. raise AssertionError("vr must be float")
  4602. else:
  4603. # Skip var_range logic for SingletonInt
  4604. # Only used for jagged layout nested tensors
  4605. range_str = ""
  4606. r = sympy_expr
  4607. is_debug = config.extended_debug_create_symbol is not None and str(
  4608. sympy_expr
  4609. ) in config.extended_debug_create_symbol.split(",")
  4610. maybe_more_info = ""
  4611. if not is_debug and os.getenv("TORCHDYNAMO_EXTENDED_ADVICE", "1") not in (
  4612. "0",
  4613. "",
  4614. ):
  4615. maybe_more_info = (
  4616. ", for more info run with "
  4617. f'TORCHDYNAMO_EXTENDED_DEBUG_CREATE_SYMBOL="{sympy_expr}" '
  4618. "or to suppress this message run with "
  4619. 'TORCHDYNAMO_EXTENDED_ADVICE="0"'
  4620. )
  4621. sloc, maybe_extra_debug = self._get_stack_summary(is_debug)
  4622. self.log.info(
  4623. "create_symbol %s = %s for %s %s %s%s%s",
  4624. sympy_expr,
  4625. val,
  4626. source.name,
  4627. range_str,
  4628. sloc,
  4629. maybe_more_info,
  4630. maybe_extra_debug,
  4631. stack_info=is_debug,
  4632. )
  4633. trace_structured(
  4634. "create_symbol",
  4635. metadata_fn=lambda: {
  4636. "symbol": str(sympy_expr),
  4637. "val": repr(val),
  4638. "vr": range_str,
  4639. "source": source.name,
  4640. "user_stack": structured.from_traceback(
  4641. TracingContext.extract_stack()
  4642. ),
  4643. "stack": structured.from_traceback(
  4644. CapturedTraceback.extract(skip=1).summary()
  4645. ),
  4646. },
  4647. )
  4648. self.counter["create_symbol"] += 1
  4649. else:
  4650. # This implements duck-shaping: input sizes that match are assigned
  4651. # the same symint
  4652. r = self.val_to_var[val]
  4653. self.source_to_var[source_name] = r
  4654. self.log.debug("create_symbol %s duck sized %s", r, source.name)
  4655. if isinstance(r, sympy.Symbol):
  4656. r_sources = self.var_to_sources[r]
  4657. r_sources.append(source)
  4658. if not source.is_ephemeral() and r_sources[0].is_ephemeral():
  4659. # prefer non-ephemeral source first since it may be guarded on later
  4660. r_sources[0], r_sources[-1] = r_sources[-1], r_sources[0]
  4661. # This ensures we get zeros in symbol_guard_counts, which makes
  4662. # some queries simpler (since we will accumulate mass on 0 this
  4663. # way)
  4664. self.symbol_guard_counter[r] = 0
  4665. if isinstance(symbolic_context, StatefulSymbolicContext) and source_name:
  4666. symbolic_context.shape_env_to_source_to_symbol_cache[id(self)][
  4667. source_name
  4668. ] = r
  4669. return r
  4670. def add_backed_var_to_val(self, expr: sympy.Symbol, val: int) -> None:
  4671. """Adds a new symbol to the symbolic environment."""
  4672. log.debug("add_backed_var_to_val %s %s", expr, val, stack_info=True)
  4673. if expr in self.backed_var_to_val:
  4674. raise AssertionError(f"{expr} already exists")
  4675. self.backed_var_to_val[expr] = sympy.Integer(val)
  4676. @property
  4677. @deprecated(
  4678. "var_to_val is deprecated, use backed_var_to_val instead",
  4679. category=FutureWarning,
  4680. )
  4681. def var_to_val(self) -> dict[sympy.Symbol, sympy.Integer]:
  4682. """Deprecated: use backed_var_to_val instead."""
  4683. return self.backed_var_to_val
  4684. @deprecated(
  4685. "add_var_to_val is deprecated, use add_backed_var_to_val instead",
  4686. category=FutureWarning,
  4687. )
  4688. def add_var_to_val(self, expr: sympy.Symbol, val: int) -> None:
  4689. """Deprecated: use add_backed_var_to_val instead."""
  4690. return self.add_backed_var_to_val(expr, val)
  4691. def _debug_name(self, source: Source) -> str:
  4692. src_name = source.name
  4693. return self.source_name_to_debug_name.get(src_name, src_name)
  4694. def _render_range_for_constraint_violation(
  4695. self, source: Source, c: Union[StrictMinMaxConstraint, RelaxedUnspecConstraint]
  4696. ) -> str:
  4697. if isinstance(c, StrictMinMaxConstraint):
  4698. lower, upper = c.vr.lower, c.vr.upper
  4699. default = self._default_value_range()
  4700. if lower <= default.lower:
  4701. lower = None
  4702. if upper >= default.upper:
  4703. upper = None
  4704. c_render = (
  4705. f"{self._debug_name(source)} = {source.name} in the specified range"
  4706. )
  4707. if lower is not None and upper is not None:
  4708. c_render += f" {lower} <= {self._debug_name(source)} <= {upper}"
  4709. elif lower is None and upper is not None:
  4710. c_render += f" {self._debug_name(source)} <= {upper}"
  4711. elif lower is not None and upper is None:
  4712. c_render += f" {lower} <= {self._debug_name(source)}"
  4713. return c_render
  4714. return c.render(source)
  4715. def produce_guards(self, *args: Any, **kwargs: Any) -> list[str]:
  4716. """
  4717. Like produce_guards_verbose, but only returns the non-verbose python guard expressions
  4718. (no verbose guards produced.)
  4719. """
  4720. return self.produce_guards_verbose(*args, **kwargs, langs=("python",))[0].exprs
  4721. def produce_guards_verbose(
  4722. self,
  4723. placeholders: Sequence[FakeTensor],
  4724. sources: Sequence[Source],
  4725. source_ref: Callable[[Source], str] = lambda n: n.name,
  4726. *,
  4727. guards: Optional[list[ShapeGuard]] = None,
  4728. input_contexts: Optional[DimList[SymbolicContext]] = None,
  4729. # Encodes user-specified input shape equations of the form s = s' and s = fn(s').
  4730. # (See docs on EqualityConstraint for details of the encoding.)
  4731. equalities_inputs: Optional[EqualityConstraint] = None,
  4732. _simplified: bool = False,
  4733. # Indicates if we should produce guards for known static values.
  4734. ignore_static: bool = True,
  4735. langs: tuple[str, ...] = ("python", "verbose_python"),
  4736. ) -> list[_ShapeGuardsHelper]:
  4737. """
  4738. Generates a list of guards strings which, when evaluated in a context that
  4739. defines tensors for all the sources, returns True or False depending
  4740. on if the guards in the list evaluated to True or not. Primarily used by Dynamo,
  4741. but this is also helpful for manual testing of guards (see
  4742. evaluate_guards_for_args)
  4743. For convenience in testing, a source is allowed to be a str,
  4744. in which case we will assume it is a LocalSource
  4745. simplified lets you omit duck sizing, equality and 0/1 guards.
  4746. This is useful for testing when you don't care about the boilerplate
  4747. guards, and it may be helpful for user output too (be careful though;
  4748. some equality guards are nontrivial! It would be nice to get simplified
  4749. output to print them too). It's private because it's not
  4750. intended for normal use
  4751. Returns guards in python and python with verbose comments (verbose) by
  4752. default.
  4753. """
  4754. self.log.info("produce_guards")
  4755. # Check if we get to the same ShapeEnv state by replaying the recorded events.
  4756. # This will create a new ShapeEnv instance, and call all recorded function
  4757. # calls on this new instance. Finally, it will check whether this new instance
  4758. # has equal state.
  4759. #
  4760. # It's important that we do it in the beginning of this function, since it modifies
  4761. # self.dim_constraints through its execution. Changes that happen in this method
  4762. # aren't interesting, since this is the function call we wish to reproduce at the
  4763. # end. If we wish to simply reproduce ShapeEnv instances even after this call,
  4764. # this method should also be recorded.
  4765. if self.check_recorded_events:
  4766. shape_env = replay_shape_env_events(self.events)
  4767. self.check_equal(shape_env)
  4768. if len(placeholders) != len(sources):
  4769. raise AssertionError(f"len({placeholders}) != len({sources})")
  4770. Tensorlike = (torch.Tensor, FakeTensorMeta)
  4771. def _create_no_constraints_context(t: Tensor) -> StatelessSymbolicContext:
  4772. return StatelessSymbolicContext(
  4773. # Ignored; only the constraints part is relevant below.
  4774. dynamic_sizes=[DimDynamic.DYNAMIC] * t.dim(),
  4775. dynamic_strides=[DimDynamic.INFER_STRIDE] * t.dim(),
  4776. constraint_sizes=[None] * t.dim(),
  4777. constraint_strides=[None] * t.dim(),
  4778. )
  4779. # Expand optional inputs, or verify invariants are upheld
  4780. if input_contexts is None:
  4781. # pyrefly: ignore [bad-assignment]
  4782. input_contexts = [
  4783. # pyrefly: ignore [bad-argument-type]
  4784. _create_no_constraints_context(t) if isinstance(t, Tensorlike) else None
  4785. for t in placeholders
  4786. ]
  4787. else:
  4788. if len(input_contexts) != len(placeholders):
  4789. raise AssertionError("len(input_contexts) != len(placeholders)")
  4790. for i, (t, context) in enumerate(zip(placeholders, input_contexts)):
  4791. if isinstance(t, Tensorlike):
  4792. if context is None:
  4793. input_contexts[i] = _create_no_constraints_context(t)
  4794. else:
  4795. if not isinstance(t, (SymInt, int, SymFloat, float)):
  4796. raise AssertionError(
  4797. f"Expected SymInt, int, SymFloat, or float, got {type(t)}"
  4798. )
  4799. if isinstance(context, list):
  4800. raise AssertionError("context must not be a list")
  4801. # It took a lot of sweat to figure out the algorithm here. Let's
  4802. # explain how it works.
  4803. #
  4804. # The ShapeEnv lifecycle looks something like this:
  4805. #
  4806. # - For each input, you either generate a fresh Sympy symbol (s0) to
  4807. # represent its value (a binding site), or you reuse some
  4808. # preexisting symbol or expression, skipping the symbol allocation
  4809. # (e.g., duck sizing to a preexisting symbol, or expressing a
  4810. # stride as a multiplication of a separate stride and size.)
  4811. # Naively, you might expect to bind a fresh Sympy symbol for
  4812. # every input, but this is fairly wasteful as most of these
  4813. # symbols immediately simplify away, and if you don't eagerly
  4814. # specialize, e.g., 0/1 symbols, you end up with very complicated
  4815. # expressions that are not optimizable in practice.
  4816. #
  4817. # - You perform some compute on these symbols, occasionally
  4818. # introducing guards on boolean expressions on these symbols.
  4819. # In particular, whenever we guard on equality (_maybe_guard_rel),
  4820. # we can simplify shapes; e.g., when s0 == s1 * 2, we can now
  4821. # replace all occurrences of s0 with s1 * 2. Sometimes, a
  4822. # boolean expression evaluation doesn't introduce a guard, as
  4823. # the guard is already entailed by the simplifications we have
  4824. # applied.
  4825. #
  4826. # - In the end, you have a bunch of replacements (saying how to
  4827. # simplify shapes) and a bunch of guards (all the equality guards
  4828. # are trivial, because they're covered by the replacements).
  4829. #
  4830. # From the ShapeEnv, we must generate a Python expression that, when
  4831. # evaluated on a set of inputs, tells us whether or not these boolean
  4832. # expressions would have evaluated in the same way. However,
  4833. # we cannot easily compute this, as we elide recording boolean
  4834. # expressions when we think they are vacuously true. Thus, we seek
  4835. # an approximation: we must generate an expression, if true, would have
  4836. # produced an "equivalent" ShapeEnv, which would answer guard
  4837. # expressions in the same way.
  4838. #
  4839. # Our notion of equivalence is a bit subtle. For example, consider
  4840. # the ShapeEnv created from an input of size (5, 4) versus (4, 4)
  4841. # (no other guards.) Duck sizing would generate (s0, s1) in the first
  4842. # case but (s0, s0) in the second. We do NOT assume that size
  4843. # variables are disjoint; so in fact a graph that assumes the input
  4844. # could be (s0, s1) subsumes (s0, s0) (setting s0 == s1), but not
  4845. # vice versa. However, consider an analogous case (1,) versus (2,).
  4846. # Duck sizing generates (1,) and (s0,); the (s0,) graph does NOT
  4847. # subsume the (1,) graph because we assume that any size variables
  4848. # is NOT 0/1 (and make simplifications according to this; e.g., if
  4849. # we queried s0 == 0, we would immediately return False without
  4850. # returning a guard.)
  4851. #
  4852. # So, it is perhaps easier to flip things on their head: the guard
  4853. # expressions we generate here say what simplifications are valid,
  4854. # and what are not. Below, we explain each of the guard expressions
  4855. # we generate
  4856. # TODO: Make this more efficient by binding all the size/stride/offsets
  4857. # to locals before performing tests on them.
  4858. from torch._dynamo.source import TensorProperty, TensorPropertySource
  4859. # Actual codegen must be delayed as we don't necessarily know what
  4860. # the symbol mapping is
  4861. input_guards = []
  4862. symbol_to_source: dict[sympy.Symbol, list[Source]] = collections.defaultdict(
  4863. list
  4864. )
  4865. symbol_to_constraints: defaultdict[sympy.Symbol, set[Constraint]] = (
  4866. collections.defaultdict(set)
  4867. )
  4868. constraint_violations: list[tuple[bool, str, Callable[[], str]]] = []
  4869. printers: list[_ShapeGuardPrinter] = []
  4870. py_printer = ShapeGuardPythonPrinter(
  4871. symbol_to_source, source_ref, self.var_to_sources
  4872. )
  4873. for lang in langs:
  4874. if lang in ["python", "verbose_python"]:
  4875. printers.append(py_printer)
  4876. elif lang == "cpp":
  4877. printers.append(
  4878. _ShapeGuardCppPrinter(
  4879. symbol_to_source, source_ref, self.var_to_sources
  4880. )
  4881. )
  4882. else:
  4883. raise NotImplementedError(f"Unknown lang: {lang}")
  4884. def record_constraint_violation(
  4885. warn_only: bool,
  4886. debug_name: str,
  4887. msg: str,
  4888. hint: Optional[Callable[[], str]] = None,
  4889. ) -> None:
  4890. constraint_violations.append(
  4891. (warn_only, debug_name, lambda: f"{msg}{hint()}" if hint else msg)
  4892. )
  4893. def is_dim(src: object) -> TypeGuard[TensorPropertySource]:
  4894. return (
  4895. isinstance(src, TensorPropertySource)
  4896. and src.prop is TensorProperty.SIZE
  4897. )
  4898. if equalities_inputs:
  4899. source_index = {}
  4900. for i, src in enumerate(sources):
  4901. source_index[src.name] = i
  4902. def get_expression(tensor_dim_src: Source) -> sympy.Expr:
  4903. fake = placeholders[source_index[tensor_dim_src.base.name]] # type: ignore[attr-defined]
  4904. if tensor_dim_src.idx is None: # type: ignore[attr-defined]
  4905. raise AssertionError("tensor_dim_src.idx must not be None")
  4906. symint = fake.shape[tensor_dim_src.idx] # type: ignore[attr-defined]
  4907. if isinstance(symint, torch.SymInt):
  4908. return symint.node.expr
  4909. else:
  4910. if type(symint) is not int:
  4911. raise AssertionError(f"Expected int, got {type(symint)}")
  4912. return sympy.Integer(symint)
  4913. for src1, src2 in equalities_inputs.source_pairs:
  4914. expr1, expr2 = get_expression(src1), get_expression(src2) # type: ignore[]
  4915. # Check whether given input shape values satisfy a specified equation s = s'.
  4916. # - Raise when the equation was violated by the given input shape values.
  4917. # - Otherwise issue a guard to constrain them.
  4918. concrete_val = self.evaluate_expr(sympy.Eq(expr1, expr2))
  4919. if not concrete_val:
  4920. raise ConstraintViolationError(
  4921. f"{src1.name} = {expr1 if isinstance(expr1, int) else expr1.xreplace(self.backed_var_to_val)}"
  4922. " is not equal to "
  4923. f"{src2.name} = {expr2 if isinstance(expr2, int) else expr2.xreplace(self.backed_var_to_val)}"
  4924. )
  4925. for srcEq, root, fn in equalities_inputs.derived_equalities:
  4926. expr1 = get_expression(srcEq)
  4927. # recall that root is either a phantom symbol or an input source
  4928. if isinstance(root, sympy.Symbol):
  4929. expr2, debug_name = root, self.var_to_sources[root][0].name
  4930. elif isinstance(root, sympy.Integer):
  4931. expr2, debug_name = root, str(root)
  4932. else:
  4933. expr2, debug_name = get_expression(root), self._debug_name(root)
  4934. expr2_ = fn(expr2)
  4935. # Check whether given input shape values satisfy a specified equation s = fn(s').
  4936. # - Raise when the equation was violated by the given input shape values.
  4937. # - Otherwise issue a guard to constrain them.
  4938. concrete_val = self.evaluate_expr(sympy.Eq(expr1, expr2_))
  4939. if not concrete_val:
  4940. raise ConstraintViolationError(
  4941. f"Expected input {srcEq.name} to be equal to "
  4942. f"{fn(sympy.Symbol(debug_name))}, "
  4943. f"where {debug_name} = {expr2.xreplace(self.backed_var_to_val)}, "
  4944. f"but got {expr1.xreplace(self.backed_var_to_val)}"
  4945. )
  4946. for phantom_symbol in equalities_inputs.phantom_symbols:
  4947. if isinstance(phantom_symbol, sympy.Symbol):
  4948. # we created additional phantom symbols that are not input shape dimensions
  4949. symbol_to_source[phantom_symbol].extend(
  4950. self.var_to_sources[phantom_symbol]
  4951. )
  4952. # How do we know what the value of s0 is? Fresh variables can only be
  4953. # bound by inputs, so there MUST be some other input which binds the
  4954. # variable. If there is no such input, this is an error in our
  4955. # system. We record where all symbols come from, to help you diagnose
  4956. # why those symbols didn't occur.
  4957. #
  4958. # In fact, generally speaking it is only possible for the "outermost"
  4959. # user of a ShapeEnv to evaluate the guards, because some inputs may
  4960. # not be available to inner levels. For example, Dynamo can guard on
  4961. # tensors that never actually become graph arguments (they are
  4962. # pruned). In this case, only Dynamo knows about these arguments.
  4963. def track_symint(
  4964. source: Source, val: IntLikeType, constraint: DimConstraint = None
  4965. ) -> None:
  4966. log.debug(
  4967. "track_symint %s %s %s",
  4968. LazyString(lambda: source.name),
  4969. val,
  4970. constraint,
  4971. )
  4972. if isinstance(val, SymInt) and not is_symbolic(val):
  4973. raise AssertionError("val must be symbolic if it is a SymInt")
  4974. if isinstance(val, SymInt) and val.node.maybe_as_int() is not None:
  4975. val = val.node.maybe_as_int()
  4976. if isinstance(val, SymInt):
  4977. s = val.node.expr
  4978. if isinstance(s, sympy.Symbol):
  4979. symbol_to_source[s].append(source)
  4980. if constraint is not None and not isinstance(
  4981. constraint, RelaxedUnspecConstraint
  4982. ):
  4983. symbol_to_constraints[s].add(constraint)
  4984. else:
  4985. constraint_violated = False
  4986. if isinstance(constraint, StrictMinMaxConstraint):
  4987. # try inferring the ranges of the expr s
  4988. sym_vrs = {
  4989. x: self.var_to_range.get(x, None) for x in s.free_symbols
  4990. }
  4991. if any(vr is None for vr in sym_vrs.values()):
  4992. # some of the free symbols in s don't have ranges
  4993. constraint_violated = True
  4994. elif isinstance(constraint, RelaxedUnspecConstraint):
  4995. if s.is_number:
  4996. i = int(s)
  4997. # Don't complain about 0/1 specialization, we
  4998. # expect to have to compile in this case anyway
  4999. if i not in (0, 1):
  5000. constraint_violated = True
  5001. if constraint_violated:
  5002. if constraint is None:
  5003. raise AssertionError("constraint must not be None")
  5004. def hint(s: sympy.Expr) -> str:
  5005. sexpr = py_printer.doprint(s)
  5006. return f"{sexpr}."
  5007. var_with_range = self._render_range_for_constraint_violation(
  5008. source, constraint
  5009. )
  5010. msg = (
  5011. f"Not all values of {var_with_range} are valid because "
  5012. f"{self._debug_name(source)} was inferred to be equal to "
  5013. )
  5014. record_constraint_violation(
  5015. constraint.warn_only,
  5016. self._debug_name(source),
  5017. msg,
  5018. hint=functools.partial(hint, s),
  5019. )
  5020. input_guards.append((source, s))
  5021. else:
  5022. s = sympy.Integer(val)
  5023. input_guards.append((source, s))
  5024. constraint_violated = False
  5025. if isinstance(constraint, StrictMinMaxConstraint):
  5026. if not (
  5027. s == constraint.vr.lower == constraint.vr.upper
  5028. ): # allow static constraints
  5029. constraint_violated = True
  5030. elif isinstance(constraint, RelaxedUnspecConstraint):
  5031. # Don't complain about 0/1 specialization, we
  5032. # expect to have to compile in this case anyway
  5033. if val not in (0, 1):
  5034. constraint_violated = True
  5035. if constraint_violated:
  5036. if constraint is None:
  5037. raise AssertionError("constraint must not be None")
  5038. var_with_range = self._render_range_for_constraint_violation(
  5039. source, constraint
  5040. )
  5041. user_stack = self.specialization_stacks.get(source, None)
  5042. msg = (
  5043. f"You marked {self._debug_name(source)} as dynamic but your code "
  5044. f"specialized it to be a constant ({val}). If you're using mark_dynamic, "
  5045. f"either remove it or use maybe_mark_dynamic. If you're using Dim.DYNAMIC, "
  5046. f"replace it with either Dim.STATIC or Dim.AUTO."
  5047. + (
  5048. "\n\nUser stack:\n" + "".join(user_stack.format())
  5049. if user_stack
  5050. else ""
  5051. )
  5052. )
  5053. record_constraint_violation(
  5054. constraint.warn_only, self._debug_name(source), msg
  5055. )
  5056. def track_symfloat(source: Source, val: FloatLikeType) -> None:
  5057. log.debug("track_symfloat %s %s", LazyString(lambda: source.name), val)
  5058. if isinstance(val, SymFloat) and not is_symbolic(val):
  5059. raise AssertionError("val must be symbolic if it is a SymFloat")
  5060. if isinstance(val, SymFloat) and val.node.maybe_as_float() is not None:
  5061. val = val.node.maybe_as_float()
  5062. if isinstance(val, SymFloat):
  5063. s = val.node.expr
  5064. if isinstance(s, sympy.Symbol):
  5065. symbol_to_source[s].append(source)
  5066. input_guards.append((source, s))
  5067. else:
  5068. s = sympy.Float(val)
  5069. input_guards.append((source, s))
  5070. # pyrefly: ignore [no-matching-overload]
  5071. for t, source, context in zip(placeholders, sources, input_contexts):
  5072. if isinstance(source, str):
  5073. from torch._dynamo.source import LocalSource
  5074. source = LocalSource(source)
  5075. if not isinstance(source, Source):
  5076. raise AssertionError(f"Expected Source, got {type(source)}")
  5077. if t is None:
  5078. continue
  5079. if isinstance(t, (SymInt, int)):
  5080. constraint = (
  5081. None if context is None else getattr(context, "constraint", None)
  5082. )
  5083. track_symint(source, t, constraint)
  5084. continue
  5085. elif isinstance(t, (SymFloat, float)):
  5086. track_symfloat(source, t)
  5087. continue
  5088. if not isinstance(t, Tensorlike):
  5089. raise AssertionError(f"Expected Tensorlike, got {type(t)}")
  5090. if is_traceable_wrapper_subclass(t):
  5091. from torch._dynamo.source import AttrSource
  5092. if not isinstance(context, SubclassSymbolicContext):
  5093. raise AssertionError(
  5094. f"Expected SubclassSymbolicContext, got {type(context)}"
  5095. )
  5096. # For subclasses, we need to track symints on BOTH the outer
  5097. # and inner tensors.
  5098. # TODO: type this better
  5099. sources_tensors_constraints: list[tuple[Source, Any, Any, Any]] = [
  5100. (source, t, context.constraint_sizes, context.constraint_strides)
  5101. ]
  5102. attrs, _ = t.__tensor_flatten__()
  5103. for attr in attrs:
  5104. inner_t = getattr(t, attr)
  5105. inner_context = context.inner_contexts[attr]
  5106. sources_tensors_constraints.append(
  5107. (
  5108. AttrSource(source, attr),
  5109. inner_t,
  5110. inner_context.constraint_sizes, # type: ignore[attr-defined]
  5111. inner_context.constraint_strides, # type: ignore[attr-defined]
  5112. )
  5113. )
  5114. else:
  5115. sources_tensors_constraints = [
  5116. (source, t, context.constraint_sizes, context.constraint_strides) # type: ignore[attr-defined]
  5117. ]
  5118. for (
  5119. src,
  5120. curr_t,
  5121. constraint_size,
  5122. constraint_stride,
  5123. ) in sources_tensors_constraints:
  5124. if is_sparse_any(curr_t):
  5125. for i, ss in enumerate(curr_t.size()):
  5126. property_source = TensorPropertySource(
  5127. src, TensorProperty.SIZE, i
  5128. )
  5129. track_symint(property_source, ss, constraint_size[i])
  5130. else:
  5131. for i, ss in enumerate(curr_t.size()):
  5132. property_source = TensorPropertySource(
  5133. src, TensorProperty.SIZE, i
  5134. )
  5135. track_symint(property_source, ss, constraint_size[i])
  5136. for i, ss in enumerate(curr_t.stride()):
  5137. property_source = TensorPropertySource(
  5138. src, TensorProperty.STRIDE, i
  5139. )
  5140. track_symint(property_source, ss, constraint_stride[i])
  5141. track_symint(
  5142. TensorPropertySource(src, TensorProperty.STORAGE_OFFSET),
  5143. curr_t.storage_offset(),
  5144. )
  5145. # 1. Every input must equal the final simplified symbolic expression
  5146. # stored on the placeholder. Given a placeholder (s0*2, s1),
  5147. # if we have an input (2, 3), we must show s0*2 == 2 and s1 == 3.
  5148. # This does a lot of work: it covers duck sizing and equality guards.
  5149. all_exprs: list[list[str]] = [[] for _ in langs]
  5150. self.dim_constraints = DimConstraints(
  5151. symbol_to_source,
  5152. self.backed_var_to_val,
  5153. set(symbol_to_constraints.keys()),
  5154. self.source_name_to_debug_name,
  5155. )
  5156. if not _simplified:
  5157. for source, expr in input_guards:
  5158. srcname = source.name
  5159. if self._translation_validation_enabled:
  5160. # Ignore sources that were not turned into SymInts.
  5161. if srcname in self.source_to_symbol:
  5162. self._add_target_expr(
  5163. sympy.Eq(self.source_to_symbol[srcname], expr)
  5164. )
  5165. # Small optimization
  5166. if (
  5167. isinstance(expr, sympy.Symbol)
  5168. and symbol_to_source.get(expr)
  5169. and source == symbol_to_source[expr][0]
  5170. ):
  5171. continue
  5172. # This logic excludes static values found on tensors from guarding, because
  5173. # dynamo's check_tensor_fn does that (see guards.cpp).
  5174. # However, for non tensor sources, we still need to guard here.
  5175. if ignore_static and isinstance(source, TensorPropertySource):
  5176. if expr.is_number:
  5177. self.log.debug(
  5178. "Skipping guard %s", f"{source_ref(source)} == {expr}"
  5179. )
  5180. continue
  5181. if is_dim(source):
  5182. self.dim_constraints.add_equality(source, expr)
  5183. for exprs, printer, lang in zip(all_exprs, printers, langs):
  5184. res = f"{printer.print_source(source)} == {printer.doprint(expr)}"
  5185. if lang == "verbose_python":
  5186. if (s0 := self.source_to_var.get(srcname)) is not None:
  5187. if source != self.var_to_sources[s0][0]:
  5188. res = (
  5189. f"{res} # duck sizing added this equality because these "
  5190. f"variables had the same size {self.backed_var_to_val[s0]} "
  5191. "(to avoid this specialization, set torch.fx.experimental._config.use_duck_shape = False)"
  5192. )
  5193. elif (sloc := self.replacements_slocs.get(s0)) is not None:
  5194. res = f"{res} # {sloc}"
  5195. else:
  5196. res = f"{res} # (unknown var {s0}, please file a bug)"
  5197. else:
  5198. res = f"{res} # (unknown source {srcname}, please file a bug)"
  5199. exprs.append(res)
  5200. if (
  5201. isinstance(source, TensorPropertySource)
  5202. and source.prop is TensorProperty.SIZE
  5203. and equalities_inputs
  5204. and len(expr.free_symbols) == 1
  5205. ):
  5206. symbol = next(iter(expr.free_symbols))
  5207. if (
  5208. isinstance(expr, sympy.Symbol)
  5209. and expr in symbol_to_constraints
  5210. and not equalities_inputs.is_equal(
  5211. source, symbol_to_source[expr][0]
  5212. )
  5213. ):
  5214. msg = (
  5215. f"The values of {self._debug_name(source)} = {source.name} and "
  5216. f"{self._debug_name(symbol_to_source[expr][0])} = {symbol_to_source[expr][0].name} "
  5217. "must always be equal."
  5218. )
  5219. record_constraint_violation(
  5220. equalities_inputs.warn_only, self._debug_name(source), msg
  5221. )
  5222. if (
  5223. not isinstance(expr, sympy.Symbol)
  5224. and symbol in symbol_to_constraints
  5225. and not equalities_inputs.is_derived(
  5226. source,
  5227. symbol_to_source[symbol][0],
  5228. lambda x: expr.xreplace({symbol: x}),
  5229. )
  5230. ):
  5231. src = symbol_to_source[symbol][0]
  5232. msg = (
  5233. f"The values of {self._debug_name(source)} = {source.name} must always be related to "
  5234. f"the values of {self._debug_name(src)} = {src.name} by "
  5235. f"{self._debug_name(source)} = {expr.xreplace({symbol: sympy.sympify(self._debug_name(src))})}."
  5236. )
  5237. record_constraint_violation(
  5238. equalities_inputs.warn_only, self._debug_name(source), msg
  5239. )
  5240. # NB: Not necessary to report constraint violations here:
  5241. # constraints are guaranteed to be on symbols (we've already
  5242. # caught constants and non-atomic expressions), so we only
  5243. # have relational constraints, but we don't support those
  5244. # at the moment
  5245. # 2. Every guard must evaluate to True (but remember many guards
  5246. # like s0 == s1*2 because trivial due to simplification)
  5247. issued = set()
  5248. def issue_guard(guard: ShapeGuard) -> None:
  5249. expr = self.simplify(guard.expr)
  5250. # Avoid re-issuing the same guard.
  5251. if expr in issued:
  5252. return
  5253. issued.add(expr)
  5254. try:
  5255. is_trivial = False
  5256. if any(
  5257. is_dim(source)
  5258. for s in expr.free_symbols
  5259. for source in symbol_to_source[s]
  5260. ):
  5261. if self.dim_constraints is None:
  5262. raise AssertionError("dim_constraints must not be None")
  5263. is_trivial = self.dim_constraints.add(expr)
  5264. for exprs, printer, lang in zip(all_exprs, printers, langs):
  5265. guard_expr = printer.doprint(expr)
  5266. if lang == "verbose_python":
  5267. guard_expr = f"{guard_expr} # {guard.sloc}"
  5268. exprs.append(guard_expr)
  5269. self._add_target_expr(expr)
  5270. # A non-relational constraint on a single sizevar can violate
  5271. # a constraint
  5272. if not is_trivial and len(expr.free_symbols) == 1:
  5273. symbol = next(iter(expr.free_symbols))
  5274. source = symbol_to_source[symbol][0]
  5275. constraints = symbol_to_constraints[symbol]
  5276. for c in constraints:
  5277. if isinstance(c, StrictMinMaxConstraint):
  5278. var_with_range = (
  5279. self._render_range_for_constraint_violation(source, c)
  5280. )
  5281. msg = (
  5282. f"Not all values of {var_with_range} "
  5283. f"satisfy the generated guard {py_printer.doprint(expr)}."
  5284. )
  5285. record_constraint_violation(
  5286. c.warn_only, self._debug_name(source), msg
  5287. )
  5288. elif isinstance(c, RelaxedUnspecConstraint):
  5289. # This is fine, we allow guards here as long as it
  5290. # didn't constrain it to one value (we don't
  5291. # actually know this; this depends on our
  5292. # ValueRanges reasoning capability)
  5293. pass
  5294. else:
  5295. raise AssertionError(f"unrecognized constraint {c}")
  5296. except Exception:
  5297. self.log.warning("Failing guard allocated at %s", guard.sloc)
  5298. raise
  5299. # First, issue all guards.
  5300. # This removes all the checks that follow from bounds
  5301. # We could simply emit those and also the bounds 2 <= size when necessary
  5302. for guard in guards if guards is not None else self.guards:
  5303. if (
  5304. self._maybe_evaluate_static(
  5305. guard.expr, axioms=(), size_oblivious=guard.size_oblivious
  5306. )
  5307. is not None
  5308. ):
  5309. continue
  5310. issue_guard(guard)
  5311. # Because there are guards that export's constraint solver can suggest good fixes for, that we may have
  5312. # deferred as runtime asserts, and that produce_guards() alone won't do anything with (e.g. divisiblity guards),
  5313. # we want to send runtime asserts to export's constraint solver too. These will still stay in the graph as asserts,
  5314. # but export's constraint solver can decide whether to do anything with them (i.e. raise an error and provide
  5315. # suggested fixes, or decide it's out of scope and leave as a runtime assert in the graph).
  5316. for ra in self.deferred_runtime_asserts.get(None, []):
  5317. if self._maybe_evaluate_static(ra.expr, axioms=()) is not None:
  5318. continue
  5319. expr = self.simplify(ra.expr)
  5320. self.dim_constraints.add(expr)
  5321. # 3. Every symbol must be within its value range (this handles 0/1
  5322. # specialization too).
  5323. for symbol, sources in symbol_to_source.items():
  5324. r = self.var_to_range.get(symbol)
  5325. if r is None:
  5326. continue
  5327. vr_sloc = self.var_to_range_sloc[symbol]
  5328. if not sources:
  5329. raise AssertionError(f"sources must not be empty for symbol {symbol}")
  5330. bounds = []
  5331. rf = source_ref(sources[0])
  5332. verbose_expr = ""
  5333. if r.lower not in (-sympy.oo, -int_oo):
  5334. if any(is_dim(source) for source in sources):
  5335. self.dim_constraints.add(sympy.Ge(symbol, r.lower))
  5336. # Only print lower bound in simplified mode if it is not the
  5337. # default
  5338. if not _simplified or r.lower != self._default_value_range().lower:
  5339. bounds.append(sympy.Le(r.lower, symbol, evaluate=False))
  5340. verbose_expr = f"{r.lower} <= {rf} # {vr_sloc.lower}"
  5341. if r.upper not in (sympy.oo, int_oo):
  5342. if any(is_dim(source) for source in sources):
  5343. self.dim_constraints.add(sympy.Le(symbol, r.upper))
  5344. # nontrivial upper bound is always interesting
  5345. bounds.append(sympy.Le(symbol, r.upper, evaluate=False))
  5346. if verbose_expr:
  5347. verbose_expr = f"{r.lower} <= {rf} <= {r.upper} # {vr_sloc.lower} and {vr_sloc.upper}"
  5348. else:
  5349. verbose_expr = f"{rf} <= {r.upper} # {vr_sloc.upper}"
  5350. if bounds:
  5351. bound = sympy.And(*bounds, evaluate=False)
  5352. for exprs, printer, lang in zip(all_exprs, printers, langs):
  5353. if lang == "verbose_python":
  5354. exprs.append(verbose_expr)
  5355. else:
  5356. exprs.append(printer.doprint(bound))
  5357. # NB: verbose_exprs are done above
  5358. # Check constraints
  5359. constraints = symbol_to_constraints[symbol]
  5360. for c in constraints:
  5361. if isinstance(c, StrictMinMaxConstraint):
  5362. # TODO: With int_oo, I think this condition is a noop
  5363. # now
  5364. if not (c.vr & self._default_value_range()).issubset(r):
  5365. source = sources[0]
  5366. expr = sympy.And(
  5367. sympy.Le(r.lower, symbol), sympy.Le(symbol, r.upper)
  5368. )
  5369. guard_expr = py_printer.doprint(expr)
  5370. var_with_range = (
  5371. self._render_range_for_constraint_violation(source, c)
  5372. )
  5373. msg = f"Not all values of {var_with_range} satisfy the generated guard {guard_expr}"
  5374. record_constraint_violation(
  5375. c.warn_only,
  5376. self._debug_name(source),
  5377. msg,
  5378. )
  5379. # We NaN specialize, which means similar to 0/1 specialization we
  5380. # should assume that the float is NOT nan. This is load bearing
  5381. # if you have something like an equality guard, nan will play
  5382. # merry hell with the reasoning.
  5383. if symbol_is_type(symbol, SymT.FLOAT):
  5384. res = f"not math.isnan({py_printer.print_source(sources[0])})"
  5385. for exprs, printer, lang in zip(all_exprs, printers, langs):
  5386. if lang == "verbose_python":
  5387. exprs.append(
  5388. f"{res} # implicit guard for float input due to NaN specialization in the framework"
  5389. )
  5390. elif lang == "python":
  5391. exprs.append(res)
  5392. elif lang == "cpp":
  5393. exprs.append(f"~std::isnan({printer.print_source(sources[0])})")
  5394. else:
  5395. raise NotImplementedError(f"Unimplemented for lang: {lang}")
  5396. if constraint_violations:
  5397. warn_msgs: list[str] = []
  5398. error_msgs: list[str] = []
  5399. debug_names = set()
  5400. for warn_only, debug_name, msg_cb in constraint_violations:
  5401. if warn_only:
  5402. str_msg = f" {len(warn_msgs) + 1}. {msg_cb()}"
  5403. warn_msgs.append(str_msg)
  5404. else:
  5405. str_msg = f" - {msg_cb()}"
  5406. error_msgs.append(str_msg)
  5407. # pyrefly: ignore [bad-argument-type]
  5408. debug_names.add(debug_name)
  5409. if len(error_msgs) > 0:
  5410. debug_names_str = ", ".join(sorted(debug_names))
  5411. err = "\n".join(error_msgs)
  5412. raise ConstraintViolationError(
  5413. f"Constraints violated ({debug_names_str})! "
  5414. 'For more information, run with TORCH_LOGS="+dynamic".\n'
  5415. f"{err}"
  5416. )
  5417. elif len(warn_msgs) > 0:
  5418. log.debug("%s Warning only constraints violated", len(warn_msgs))
  5419. signpost_event(
  5420. "dynamic",
  5421. "produce_guards",
  5422. {
  5423. **self.co_fields,
  5424. **self.counter,
  5425. "num_guards": len(all_exprs[0]),
  5426. "free_symbols": sum(1 for v in symbol_to_source.values() if v),
  5427. # The keys are meaningless from an aggregate perspective, so
  5428. # don't include them. Biggest first.
  5429. "symbol_guard_counts": sorted(
  5430. self.symbol_guard_counter.values(), reverse=True
  5431. ),
  5432. },
  5433. )
  5434. if self._translation_validation_enabled:
  5435. from torch.fx.experimental.validator import PopulateValidator
  5436. # Add all deferred runtime assertions; these are not technically
  5437. # handled by produce_guards but we need to put them in the target
  5438. # set
  5439. for ras in self.deferred_runtime_asserts.values():
  5440. for ra in ras:
  5441. self._add_target_expr(ra.expr)
  5442. # Add value range bound guards for all symbols with no trivial bounds.
  5443. # Reason: '_maybe_evaluate_static' may eliminate guards based on the
  5444. # refined value ranges.
  5445. for sym, vr in self.var_to_range.items():
  5446. if vr.lower not in (-sympy.oo, -int_oo):
  5447. self._add_target_expr(sympy.Le(vr.lower, sym))
  5448. if vr.upper not in (sympy.oo, int_oo):
  5449. self._add_target_expr(sympy.Le(sym, vr.upper))
  5450. # Before validating, populate the input of the validator with the
  5451. # built FX graph.
  5452. with fx_traceback.preserve_node_meta():
  5453. PopulateValidator(self.graph, self.validator).run()
  5454. # Only run translation validation when we are not passing custom guards
  5455. if guards is None:
  5456. self._check_translation_validate()
  5457. helpers: list[_ShapeGuardsHelper] = []
  5458. for exprs, printer, lang in zip(all_exprs, printers, langs):
  5459. if lang == "cpp":
  5460. if not isinstance(printer, _ShapeGuardCppPrinter):
  5461. raise AssertionError(
  5462. f"Expected _ShapeGuardCppPrinter, got {type(printer)}"
  5463. )
  5464. helpers.append(_CppShapeGuardsHelper(exprs, printer.source_to_symbol))
  5465. else:
  5466. helpers.append(_ShapeGuardsHelper(exprs))
  5467. return helpers
  5468. def produce_guards_expression(
  5469. self,
  5470. placeholders: Sequence[Union[SymInt, FakeTensor]],
  5471. *,
  5472. guards: Optional[list[ShapeGuard]] = None,
  5473. ignore_static: bool = True,
  5474. ) -> Optional[str]:
  5475. """
  5476. Expected to be used with evaluate_guards_expression(). Produces the guards
  5477. for the given placeholders and returns a string expression to be evaluated
  5478. by evaluate_guards_expression given concrete values for the placeholders.
  5479. """
  5480. from torch._dynamo.source import LocalSource
  5481. arg_names = [f"t{i}" for i in range(len(placeholders))]
  5482. produced_guards = self.produce_guards(
  5483. placeholders,
  5484. [LocalSource(a) for a in arg_names],
  5485. guards=guards,
  5486. ignore_static=ignore_static,
  5487. )
  5488. if produced_guards:
  5489. return " and ".join(produced_guards)
  5490. return None
  5491. def evaluate_symexpr(self, code: str) -> Union[int, float, bool]:
  5492. """
  5493. To be used by compile_fx to evaluate symexprs
  5494. """
  5495. args = {str(e): val for e, val in self.backed_var_to_val.items()}
  5496. return eval(code, SYMPY_INTERP, args)
  5497. def deserialize_symexpr(self, code: str) -> Union[SymInt, SymFloat, SymBool]:
  5498. """
  5499. To be used by compile_fx to deserialize symexprs
  5500. """
  5501. args = {
  5502. str(e): SymInt(SymNode(e, self, int, int(val), fx_node=None))
  5503. for e, val in self.backed_var_to_val.items()
  5504. }
  5505. return eval(code, SYMPY_INTERP, args)
  5506. def evaluate_guards_expression(self, code: str, args: Sequence[object]) -> bool:
  5507. """
  5508. Expected to be used with produce_guards_expression(). Evaluates an expression
  5509. generated by produce_guards_expression for the given concrete args.
  5510. """
  5511. arg_names = [f"t{i}" for i in range(len(args))]
  5512. return eval(code, SYMPY_INTERP, {"L": dict(zip(arg_names, args))})
  5513. def evaluate_guards_for_args(
  5514. self,
  5515. placeholders: Sequence[FakeTensor],
  5516. args: Sequence[Tensor],
  5517. *,
  5518. ignore_static: bool = True,
  5519. ) -> bool:
  5520. """Generate guards for a graph's placeholder values and evaluate the guards with args"""
  5521. code = self.produce_guards_expression(placeholders, ignore_static=ignore_static)
  5522. if code:
  5523. return self.evaluate_guards_expression(code, args)
  5524. return True
  5525. def get_pruned_guards(self, symints: Sequence[torch.SymInt]) -> list[ShapeGuard]:
  5526. """
  5527. Get a list of guards, but pruned so it only provides guards that
  5528. reference symints from the passed in input
  5529. """
  5530. # pyrefly: ignore [bad-assignment]
  5531. symints = {
  5532. s.node.expr for s in symints if isinstance(s.node.expr, sympy.Symbol)
  5533. }
  5534. guards = [
  5535. g for g in self.guards if all(s in symints for s in g.expr.free_symbols)
  5536. ]
  5537. return guards
  5538. def bind_symbols(
  5539. self, placeholders: Sequence[FakeTensor], args: Sequence[Tensor]
  5540. ) -> dict[sympy.Symbol, int]:
  5541. """
  5542. Given a paired list of placeholders (fake tensors with
  5543. symbolic sizes) and concrete arguments (regular tensors
  5544. with real sizes), returns a dictionary mapping each
  5545. symbol to its real value. So for example, if you
  5546. have a placeholder with size (s0, s1), binding
  5547. (2, 4) to it will give you {s0: 2, s1: 4}. This is
  5548. not guaranteed to bind ALL symbols in the ShapeEnv;
  5549. we can't bind a symbol if it doesn't occur in any placeholder,
  5550. and symbols that already have replacements won't get bindings.
  5551. This is a little duplicative with evaluate_guards but
  5552. it's different enough that it seemed cleanest to make
  5553. another copy. This assumes the guards are already checked,
  5554. though if it's cheap we'll check for shenanigans
  5555. """
  5556. bindings: dict[sympy.Symbol, int] = {}
  5557. def bind_symint(arg: object, val: object) -> None:
  5558. if isinstance(val, SymInt):
  5559. if not isinstance(arg, int):
  5560. raise AssertionError(f"Expected int, got {type(arg)}")
  5561. s = val.node.expr
  5562. if isinstance(s, sympy.Symbol):
  5563. if s in bindings:
  5564. if bindings[s] != arg:
  5565. raise AssertionError(f"{bindings[s]} != {arg}")
  5566. else:
  5567. bindings[s] = arg
  5568. elif isinstance(-s, sympy.Symbol):
  5569. if -s in bindings:
  5570. if bindings[-s] != -arg:
  5571. raise AssertionError(f"{bindings[-s]} != {-arg}")
  5572. else:
  5573. bindings[-s] = -arg
  5574. for t, arg in zip(placeholders, args):
  5575. if t is None:
  5576. continue
  5577. if isinstance(t, SymInt):
  5578. bind_symint(arg, t)
  5579. continue
  5580. if not isinstance(t, torch.Tensor):
  5581. raise AssertionError(f"Expected Tensor, got {type(t)}")
  5582. for i, s in enumerate(t.size()):
  5583. bind_symint(arg.size(i), s)
  5584. for i, s in enumerate(t.stride()):
  5585. bind_symint(arg.stride(i), s)
  5586. bind_symint(arg.storage_offset(), t.storage_offset())
  5587. return bindings
  5588. def get_nontrivial_guards(self) -> list[SympyBoolean]:
  5589. """Returns a list of guard expressions that aren't statically known (i.e. not trivial)"""
  5590. return [
  5591. self.simplify(guard.expr)
  5592. for guard in self.guards
  5593. if self._maybe_evaluate_static(
  5594. guard.expr, axioms=(), size_oblivious=guard.size_oblivious
  5595. )
  5596. is None
  5597. ]
  5598. def format_guards(self, verbose: bool = False) -> str:
  5599. """Format this shape env's guard expressions with optional traceback info if verbose"""
  5600. return "\n".join(
  5601. f" - {guard.expr}{' ' + str(guard.sloc) if verbose else ''}"
  5602. for guard in self.guards
  5603. )
  5604. def bound_sympy(
  5605. self, expr: sympy.Expr, size_oblivious: bool = False
  5606. ) -> ValueRanges:
  5607. """Given a sympy expression, computes a ValueRanges bound for what values it can be"""
  5608. # TODO: maybe it's guaranteed x in is var_to_range?
  5609. var_to_range = {x: self.var_to_range.get(x, None) for x in expr.free_symbols}
  5610. if size_oblivious:
  5611. # Clamp values of size-like variables
  5612. # NB: discarding the old upper bound in intentional, per
  5613. # https://github.com/pytorch/pytorch/pull/123675
  5614. for x in self.size_like & var_to_range.keys():
  5615. if var_to_range[x] is not None:
  5616. # NB: do NOT set upper to 2 ** 48, we're using this solely
  5617. # to determine if we can do size-like replacement, the
  5618. # upper bound is irrelevant here
  5619. var_to_range[x] = ValueRanges(2, int_oo)
  5620. return bound_sympy(expr, var_to_range) # type: ignore[arg-type]
  5621. @_lru_cache
  5622. def get_axioms(
  5623. self,
  5624. symbols: Optional[tuple[sympy.Symbol]] = None,
  5625. compute_hint: bool = False,
  5626. ) -> tuple[SympyBoolean, ...]:
  5627. """
  5628. Given the symbols in an expression, it returns all the runtime asserts that have those symbols
  5629. concatenated with all the guards.
  5630. If symbols is None, it returns all the runtime asserts (and all the guards)
  5631. """
  5632. if symbols is None:
  5633. runtime_asserts = (
  5634. r.expr for rs in self.deferred_runtime_asserts.values() for r in rs
  5635. )
  5636. else:
  5637. runtime_asserts = (
  5638. r.expr
  5639. for s in symbols
  5640. if s not in self.backed_var_to_val
  5641. for r in self.deferred_runtime_asserts.get(s, ())
  5642. )
  5643. guards: Iterator[SympyBoolean] = (g.expr for g in self.guards)
  5644. axioms: Iterator[SympyBoolean] = itertools.chain(guards, runtime_asserts)
  5645. if compute_hint:
  5646. axioms = (
  5647. canonicalize_bool_expr(a.xreplace(self.backed_var_to_val))
  5648. for a in axioms
  5649. )
  5650. return tuple(dict.fromkeys(axioms).keys())
  5651. @lru_cache(None)
  5652. def get_implications(
  5653. self, e: SympyBoolean
  5654. ) -> tuple[tuple[SympyBoolean, sympy.logic.boolalg.BooleanAtom], ...]:
  5655. """Given a expression, it returns a list of predicates that follow from it"""
  5656. equiv: dict[SympyBoolean, sympy.logic.boolalg.BooleanAtom] = {}
  5657. def add_expr(expr: SympyBoolean) -> None:
  5658. expr = canonicalize_bool_expr(expr)
  5659. if isinstance(expr, (sympy.Eq, sympy.Ne)):
  5660. # No need to canonicalize
  5661. # TODO We could further canonicalize Eq ordering the lhs and rhs somehow
  5662. # With this, we could remove the need for the commutativity part
  5663. opposite = sympy.Eq if isinstance(expr, sympy.Ne) else sympy.Ne
  5664. # Commutativity of == and !=
  5665. equiv[type(expr)(expr.lhs, expr.rhs, evaluate=False)] = sympy.true
  5666. equiv[type(expr)(expr.rhs, expr.lhs, evaluate=False)] = sympy.true
  5667. equiv[opposite(expr.lhs, expr.rhs, evaluate=False)] = sympy.false
  5668. equiv[opposite(expr.rhs, expr.lhs, evaluate=False)] = sympy.false
  5669. else:
  5670. # Expr and negation
  5671. equiv[expr] = sympy.true
  5672. # we do not pass evaluate=False like others on purpose here!
  5673. # we want not(a<b) to be a>=b and not ~(a<b).
  5674. equiv[canonicalize_bool_expr(sympy.Not(expr))] = sympy.false
  5675. add_expr(e)
  5676. # Other relational expressions this expression implies
  5677. if isinstance(e, sympy.Eq):
  5678. add_expr(sympy.Le(e.lhs, e.rhs, evaluate=False))
  5679. add_expr(sympy.Ge(e.lhs, e.rhs, evaluate=False))
  5680. elif isinstance(e, sympy.Lt):
  5681. add_expr(sympy.Le(e.lhs, e.rhs, evaluate=False))
  5682. add_expr(sympy.Ne(e.lhs, e.rhs, evaluate=False))
  5683. if e.lhs.is_integer and e.rhs.is_integer: # type: ignore[attr-defined]
  5684. add_expr(sympy.Le(e.lhs, e.rhs - 1, evaluate=False))
  5685. elif isinstance(e, sympy.Le):
  5686. add_expr(sympy.Lt(e.lhs, e.rhs + 1, evaluate=False))
  5687. return tuple(equiv.items())
  5688. def _is_nonneg_term(self, term: sympy.Expr) -> bool:
  5689. """Check if a single term is non-negative (symbol with non-neg range or non-neg constant)."""
  5690. if term.is_Symbol:
  5691. vr = self.var_to_range.get(term)
  5692. return vr is not None and vr.lower >= 0
  5693. if term.is_number:
  5694. return term >= 0
  5695. return False
  5696. def _is_nonneg_sum(self, expr: sympy.Expr) -> bool:
  5697. """
  5698. Check if expr is a sum of non-negative terms (Add of symbols with non-neg range
  5699. and non-negative constants). Returns True only for simple Add expressions.
  5700. """
  5701. if not isinstance(expr, sympy.Add):
  5702. return self._is_nonneg_term(expr)
  5703. # Check each arg in the Add
  5704. for arg in expr.args:
  5705. if not self._is_nonneg_term(arg):
  5706. return False
  5707. return True
  5708. def _maybe_fast_eval_comparison(self, expr: sympy.Basic) -> Optional[sympy.Basic]:
  5709. """
  5710. Fast path for trivial comparisons: sum of non-negative terms >= 0.
  5711. Returns sympy.true if pattern matches, None otherwise.
  5712. """
  5713. if len(expr.args) != 2:
  5714. return None
  5715. lhs, rhs = expr.args
  5716. # Handle: sum >= 0 (Ge) or 0 <= sum (Le)
  5717. if isinstance(expr, sympy.Ge) and rhs == 0:
  5718. sum_expr = lhs
  5719. elif isinstance(expr, sympy.Le) and lhs == 0:
  5720. sum_expr = rhs
  5721. else:
  5722. return None
  5723. if self._is_nonneg_sum(sum_expr):
  5724. return sympy.true
  5725. return None
  5726. @_lru_cache
  5727. def _maybe_evaluate_static(
  5728. self,
  5729. expr: sympy.Basic,
  5730. *,
  5731. unbacked_only: bool = False,
  5732. compute_hint: bool = False,
  5733. size_oblivious: bool = False,
  5734. axioms: Optional[tuple[SympyBoolean]] = None,
  5735. var_to_range: Optional[tuple[tuple[sympy.Symbol, ValueRanges]]] = None,
  5736. ) -> Optional[sympy.Basic]:
  5737. """
  5738. Tries to evaluate expr without introducing guards
  5739. If unbacked_only == True, then we only do substitutions on
  5740. unbacked SymInts (leaving regular hinted integers alone). This could
  5741. result in an expression that still contains backed SymInts, which you
  5742. could then potentially guard on.
  5743. Use compute_hint == True if you are trying to compute a non-binding
  5744. hint for the particular hint values of backed and unbacked SymInts,
  5745. e.g., if s0 happens to be 3 this run, compute_hint will substitute s0 with 3.
  5746. """
  5747. # axioms with compute hint NYE
  5748. if compute_hint and axioms:
  5749. raise AssertionError("compute_hint and axioms cannot both be set")
  5750. expr = self.simplify(expr, size_oblivious)
  5751. if compute_hint:
  5752. expr = expr.xreplace(self.backed_var_to_val).xreplace(
  5753. self.real_tensor_prop_unbacked_vals
  5754. )
  5755. expr = canonicalize_bool_expr(expr)
  5756. def resimplify_floor_div(axioms: dict[sympy.Expr, sympy.Expr]) -> None:
  5757. if not self._resimplify_floor_div_axioms:
  5758. return
  5759. self._resimplify_floor_div_axioms = False
  5760. new_items = {}
  5761. for k, v in list(axioms.items()):
  5762. # A FloorDiv in implications could have became CleanDiv at this point, due to new facts
  5763. # to the shapeEnv. This handles such issue but its not ideal. This is the only expression
  5764. # simplification that depends on the global state of shape env.
  5765. # TODO try to get rid of CleanDiv since it breaks the invariant that's simplifications of sympy
  5766. # expressions only depend on the expression itself.
  5767. if k.has(FloorDiv):
  5768. new_items.update({self.simplify(k): v})
  5769. axioms.update(new_items)
  5770. # Pattern matching
  5771. if axioms is None:
  5772. resimplify_floor_div(self.axioms)
  5773. subst = self.axioms
  5774. else:
  5775. subst = {}
  5776. for e in axioms:
  5777. if e.free_symbols.issubset(expr.free_symbols):
  5778. subst.update(dict(self.get_implications(self.simplify(e))))
  5779. resimplify_floor_div(subst)
  5780. expr = expr.xreplace(subst)
  5781. # TODO: compute hint might have gotten broken here
  5782. fs = expr.free_symbols
  5783. if not fs and (expr.is_number or expr.is_Boolean):
  5784. return expr
  5785. if var_to_range is None:
  5786. var_ranges = self.var_to_range
  5787. else:
  5788. var_ranges = dict(var_to_range)
  5789. symbol_info = tuple(
  5790. _SymbolInfo(
  5791. s,
  5792. var_ranges.get(s),
  5793. self.backed_var_to_val.get(s),
  5794. s in self.size_like,
  5795. )
  5796. for s in sorted(fs, key=str) # TODO: speed up sort?
  5797. )
  5798. r = _maybe_evaluate_static_worker(
  5799. expr, symbol_info, unbacked_only, size_oblivious
  5800. )
  5801. return r
  5802. @_lru_cache
  5803. def replace(self, expr: _SympyT) -> _SympyT:
  5804. """
  5805. Apply symbol replacements to any symbols in the given expression.
  5806. """
  5807. replacements = {}
  5808. # pyrefly: ignore [missing-attribute]
  5809. for s in expr.free_symbols:
  5810. r = self._find(s)
  5811. # Micro-optimization: only do replacements if r and s are different
  5812. # Otherwise, xreplace is not a no-op and will trigger expensive
  5813. # assumption queries if expr has a relational node.
  5814. if not r.is_Symbol or r != s:
  5815. replacements[s] = r
  5816. if replacements:
  5817. # pyrefly: ignore [missing-attribute]
  5818. return safe_expand(expr.xreplace(replacements))
  5819. else:
  5820. return expr
  5821. @_lru_cache
  5822. def _update_divisible(self) -> None:
  5823. new_divisible = set()
  5824. for k in self.divisible:
  5825. res = self.replace(k)
  5826. if not res.is_number:
  5827. new_divisible.add(k)
  5828. self.divisible = new_divisible
  5829. self._update_version_counter()
  5830. @_lru_cache
  5831. def simplify(self, expr: _SympyT, size_oblivious: bool = False) -> _SympyT:
  5832. """Use known constraints and replacements to simplify the given expr"""
  5833. expr = safe_expand(expr)
  5834. expr = self.replace(expr)
  5835. # Simplify max(0/1, x) to x when x >= 0/1. max(1, x) is a commonly introduced
  5836. # expression when creating contiguous strides.
  5837. if not size_oblivious:
  5838. min_max_replacements = {}
  5839. for atom in expr.atoms(Max): # type: ignore[has-type]
  5840. if len(atom.args) > 2:
  5841. continue
  5842. a, b = atom.args
  5843. if b == 1 or b == 0:
  5844. a, b = b, a
  5845. if a == 1 and self._maybe_evaluate_static(sympy.Ge(b, 1)):
  5846. min_max_replacements[atom] = b
  5847. if a == 0 and self._maybe_evaluate_static(sympy.Ge(b, 0)):
  5848. min_max_replacements[atom] = b
  5849. if min_max_replacements:
  5850. expr = expr.xreplace(min_max_replacements)
  5851. if expr.has(TruncToInt):
  5852. trunc_replacements = {}
  5853. for atom in expr.atoms(TruncToInt):
  5854. if isinstance(atom.args[0], IntTrueDiv):
  5855. base, divisor = atom.args[0].args
  5856. if base % divisor == 0:
  5857. trunc_replacements[atom] = CleanDiv(base, divisor)
  5858. else:
  5859. # TruncToInt(IntTrueDiv(a,b)) == FloorDiv(a, b)
  5860. trunc_replacements[atom] = FloorDiv(base, divisor)
  5861. if trunc_replacements:
  5862. expr = expr.xreplace(trunc_replacements)
  5863. # TODO it would seem that this pass is not necessary given the
  5864. # below replacement of // with /, but for nested FloorDivs
  5865. # the non-recursive replacement doesn't work, and
  5866. # recursive makes it hard to look up divisibility,
  5867. # because existing divisibility info has FloorDiv in it, not /
  5868. # for now just do a separate pass to catch common nested case
  5869. if expr.has(FloorDiv):
  5870. self._update_divisible()
  5871. div_replacements = {}
  5872. for atom in expr.atoms(FloorDiv):
  5873. base, divisor = atom.args
  5874. if isinstance(divisor, FloorDiv):
  5875. base1, divisor1 = divisor.args
  5876. if (
  5877. self.replace(Mod(base, divisor)) in self.divisible
  5878. and base == base1
  5879. and self.replace(Mod(base1, divisor1)) in self.divisible
  5880. ):
  5881. div_replacements[atom] = divisor1
  5882. if div_replacements:
  5883. expr = expr.xreplace(div_replacements)
  5884. expr = safe_expand(expr)
  5885. if expr.has(FloorDiv):
  5886. div_replacements = {}
  5887. pows = expr.atoms(sympy.Pow)
  5888. rationals = expr.atoms(sympy.Rational).difference(expr.atoms(sympy.Integer))
  5889. for fd in expr.atoms(FloorDiv):
  5890. base, divisor = fd.args
  5891. if self.replace(Mod(base, divisor)) in self.divisible:
  5892. div_replacements[fd] = CleanDiv(base, divisor)
  5893. if div_replacements:
  5894. new_expr = expr.xreplace(div_replacements)
  5895. new_expr = safe_expand(new_expr)
  5896. new_pows = new_expr.atoms(sympy.Pow)
  5897. new_rationals = new_expr.atoms(sympy.Rational).difference(
  5898. new_expr.atoms(sympy.Integer)
  5899. )
  5900. # divisions simplified away
  5901. if new_pows.issubset(pows) and new_rationals.issubset(rationals):
  5902. expr = new_expr
  5903. return expr
  5904. # TODO: overload for allow_none literal
  5905. @lru_cache(256)
  5906. def size_hint(
  5907. self, expr: sympy.Basic, *, allow_none: bool = False
  5908. ) -> Optional[sympy.Basic]:
  5909. """
  5910. Gets a size hint for a given expression from the underlying shapes we had.
  5911. Does not introduce a guard, so only use this when you can guarantee that
  5912. your code is still valid for arbitrary shapes (such as optimization decisions)
  5913. """
  5914. result_expr = safe_expand(expr).xreplace(self.backed_var_to_val)
  5915. if not result_expr.is_number:
  5916. from torch.utils._sympy.singleton_int import SingletonInt
  5917. if isinstance(result_expr, SingletonInt):
  5918. return None
  5919. r = self._maybe_evaluate_static(result_expr, compute_hint=True)
  5920. if r is not None:
  5921. return r
  5922. if allow_none:
  5923. return None
  5924. if self.real_tensor_prop_unbacked_vals:
  5925. unsound_expr = result_expr.xreplace(self.real_tensor_prop_unbacked_vals)
  5926. if not unsound_expr.free_symbols:
  5927. log.warning(
  5928. "propagate_real_tensors size_hint(%s) -> %s", expr, unsound_expr
  5929. )
  5930. trace_structured(
  5931. "propagate_real_tensors",
  5932. metadata_fn=lambda: {
  5933. "expr": repr(expr),
  5934. "result": repr(unsound_expr),
  5935. "stack": structured.from_traceback(
  5936. CapturedTraceback.extract(skip=1).summary()
  5937. ),
  5938. },
  5939. )
  5940. self.guard_or_defer_runtime_assert(
  5941. sympy.Eq(result_expr, unsound_expr),
  5942. f"propagate_real_tensors: {result_expr} == {unsound_expr}",
  5943. )
  5944. return unsound_expr
  5945. raise self._make_data_dependent_error(result_expr, expr)
  5946. return result_expr
  5947. # NB: keep in sync with size_hint
  5948. @lru_cache(256)
  5949. def has_hint(self, expr: sympy.Expr) -> bool:
  5950. result_expr = safe_expand(expr).xreplace(self.backed_var_to_val)
  5951. return (
  5952. result_expr.is_number
  5953. or self._maybe_evaluate_static(result_expr) is not None
  5954. )
  5955. def _make_data_dependent_error(
  5956. self,
  5957. expr: sympy.Basic,
  5958. unhinted_expr: sympy.Basic,
  5959. *,
  5960. expr_sym_node_id: Optional[int] = None,
  5961. ) -> GuardOnDataDependentSymNode:
  5962. # TODO: in a Dynamo context, having user code, and having the
  5963. # name of the local, will be much better
  5964. size_like_symbols = []
  5965. for s in expr.free_symbols:
  5966. stacktrace = "".join(self.var_to_stack[s].format())
  5967. self.log.debug(
  5968. "Data dependent variable '%s' allocated at:\n%s", s, stacktrace
  5969. )
  5970. if s in self.size_like:
  5971. size_like_symbols.append(s)
  5972. size_oblivious_result_msg = ""
  5973. sloc, maybe_extra_debug = self._get_stack_summary(True)
  5974. if expr.is_integer: # type: ignore[attr-defined]
  5975. desc = (
  5976. "Could not extract specialized integer from data-dependent expression"
  5977. )
  5978. else:
  5979. desc = "Could not guard on data-dependent expression"
  5980. size_oblivious_result_msg = (
  5981. "consider using data-dependent friendly APIs such as "
  5982. "guard_or_false, guard_or_true and statically_known_true."
  5983. )
  5984. msg = (
  5985. f"{desc} {expr} (unhinted: {unhinted_expr}). "
  5986. f"(Size-like symbols: {', '.join(map(str, size_like_symbols)) or 'none'})\n\n"
  5987. f"{size_oblivious_result_msg}\n"
  5988. f"Caused by: {sloc}\n"
  5989. 'For more information, run with TORCH_LOGS="dynamic"\n'
  5990. "For extended logs when we create symbols, also add "
  5991. f'TORCHDYNAMO_EXTENDED_DEBUG_CREATE_SYMBOL="{",".join(map(str, expr.free_symbols))}"\n'
  5992. "If you suspect the guard was triggered from C++, add TORCHDYNAMO_EXTENDED_DEBUG_CPP=1\n"
  5993. "For more debugging help, see "
  5994. "https://docs.google.com/document/d/1HSuTTVvYH1pTew89Rtpeu84Ht3nQEFTYhAX3Ypa_xJs/edit?usp=sharing\n"
  5995. + maybe_extra_debug
  5996. # TODO: Help text about how to use our runtime tests to fix this
  5997. # problem
  5998. )
  5999. dtrace_structured(
  6000. "guard_on_data_dependent_error",
  6001. metadata_fn=lambda: {
  6002. "expr": repr(expr),
  6003. "unhinted_expr": repr(unhinted_expr),
  6004. "expr_id": self._expr_sym_node_id,
  6005. "stack": structured.from_traceback(
  6006. CapturedTraceback.extract(skip=1).summary()
  6007. ),
  6008. },
  6009. )
  6010. return GuardOnDataDependentSymNode(expr, msg)
  6011. def _update_var_to_range(
  6012. self,
  6013. symbol: sympy.Symbol,
  6014. vr: ValueRanges,
  6015. vr_sloc: Optional[ValueRangesSLoc] = None,
  6016. *,
  6017. is_constraint: bool = False,
  6018. ) -> None:
  6019. lower, upper = vr.lower, vr.upper
  6020. # If we have a size-like unbacked SymInt, refuse to refine the range to be
  6021. # less than two. This is because when we intersect this range
  6022. # with [2, inf] for size oblivious tests, the range would be
  6023. # unsatisfiable. In other words, once you have a size-like
  6024. # unbacked SymInt, we can never learn that it is exactly zero or one,
  6025. # because we would now give inconsistent results for all size
  6026. # oblivous tests!
  6027. if upper < 2 and symbol in self.size_like:
  6028. vr = ValueRanges(lower, 2)
  6029. # Updates the range and the guards corresponding to each bound of the symbol.
  6030. if symbol not in self.var_to_range:
  6031. self.log.debug("_update_var_to_range %s = %s (new)", symbol, vr)
  6032. self.var_to_range[symbol] = vr
  6033. if vr_sloc is None:
  6034. sloc = self._get_sloc()
  6035. vr_sloc = ValueRangesSLoc(sloc, sloc)
  6036. self.var_to_range_sloc[symbol] = vr_sloc
  6037. else:
  6038. old = self.var_to_range[symbol]
  6039. new = old & vr
  6040. if new != old:
  6041. if vr_sloc is None:
  6042. sloc = self._get_sloc()
  6043. vr_sloc = ValueRangesSLoc(sloc, sloc)
  6044. if new.lower != old.lower:
  6045. self.var_to_range_sloc[symbol].lower = vr_sloc.lower
  6046. if new.upper != old.upper:
  6047. self.var_to_range_sloc[symbol].upper = vr_sloc.upper
  6048. self.var_to_range[symbol] = new
  6049. self.log.debug("_update_var_to_range %s = %s (update)", symbol, new)
  6050. if (v := self.backed_var_to_val.get(symbol)) is not None:
  6051. r = self.var_to_range[symbol]
  6052. if v not in r:
  6053. # For constraint failure, delay this for later
  6054. # TODO: Rework all of this, the constraint logic is very
  6055. # duplicative with regular reasoning
  6056. if not is_constraint:
  6057. if v not in r:
  6058. raise AssertionError(f"{v} not in {r}")
  6059. def _set_replacement(self, a: sympy.Symbol, tgt: sympy.Expr, msg: str) -> None:
  6060. """
  6061. Adds or updates a replacement for a symbol.
  6062. Use this instead of `self.replacements[a] = tgt`.
  6063. """
  6064. if tgt == self.replacements.get(a, None):
  6065. return
  6066. if a in tgt.free_symbols:
  6067. return
  6068. # Precondition: a == tgt
  6069. if not isinstance(a, sympy.Symbol):
  6070. raise AssertionError(f"Expected sympy.Symbol, got {type(a)}")
  6071. if (
  6072. self.prefer_deferred_runtime_asserts_over_guards
  6073. and not _is_supported_equivalence(tgt)
  6074. ):
  6075. return # continuing leads to placeholder shapes having complex expressions that we can't resolve
  6076. # Handles nested tensor symbolic variables which don't have
  6077. # var_to_range bounds
  6078. tgt_bound = None
  6079. if a in self.var_to_range:
  6080. src_bound = self.var_to_range[a]
  6081. # First, refine the value range of a based on the computed value range
  6082. # of tgt. This is always OK to do, even if we decide not to do the
  6083. # substitution in the end. This might be a no-op, if a already has
  6084. # a tighter bound
  6085. tgt_bound = self.bound_sympy(tgt)
  6086. self._update_var_to_range(a, tgt_bound)
  6087. # Next, check if we can update the range of free symbols in tgt
  6088. # based on the range in a. But only do it if:
  6089. # - the source bound non-trivially improves over what we get out of
  6090. # the existing bounds.
  6091. # - the replacement is univariate and we can invert the tgt expression
  6092. if not tgt_bound.issubset(src_bound) and len(tgt.free_symbols) == 1:
  6093. b = next(iter(tgt.free_symbols))
  6094. # Try to invert the equality
  6095. r = try_solve(sympy.Eq(a, tgt), b, floordiv_inequality=False)
  6096. if r is not None:
  6097. self.log.debug(
  6098. "set_replacement: solve for %s in %s == %s gives %s",
  6099. b,
  6100. a,
  6101. tgt,
  6102. r,
  6103. )
  6104. # The solution here can be non-integral, for example, if
  6105. # we have s0 = 2*s1, then s1 = s0/2. What we would like
  6106. # to do is calculated the bounds in arbitrary precision,
  6107. # and then requantize the bound to integers when we are
  6108. # done.
  6109. rat_b_bound = self.bound_sympy(r[1])
  6110. b_bound = ValueRanges(
  6111. CeilToInt(rat_b_bound.lower), FloorToInt(rat_b_bound.upper)
  6112. )
  6113. self._update_var_to_range(b, b_bound, self.var_to_range_sloc[a])
  6114. tgt_bound = self.bound_sympy(tgt)
  6115. if not tgt_bound.issubset(src_bound):
  6116. raise AssertionError(
  6117. f"{tgt_bound=} not a subset of {src_bound=}"
  6118. )
  6119. # TODO: Should we propagate size-like-ness?
  6120. #
  6121. # Pros: if u0 is size-like, intuitively u0 == u1 should cause u1
  6122. # to become size-like.
  6123. #
  6124. # Cons: if u0 is size-like, what about u0 - 1 == u1? You CAN'T
  6125. # propagate in this case, because what if u0 == 0, then u1 is negative
  6126. # and clearly isn't a size. So, at minimum, any f(x) whose value
  6127. # range isn't [0, inf] given x in [0, inf] cannot propagate
  6128. # size-like-ness. But there are many situations where you could
  6129. # imagine u1 is going to be size-like and actually you just didn't
  6130. # have a refined enough value range on u0. Since even innocuous
  6131. # looking arithmetic operations can destroy size-like-ness, it's
  6132. # best to not propagate it at all and force the user to annotate it
  6133. # as necessary.
  6134. #
  6135. # Compromise: we preserve size-like-ness only for exact equality
  6136. # and nothing else.
  6137. if a in self.size_like and isinstance(tgt, sympy.Symbol):
  6138. self.size_like.add(tgt)
  6139. elif isinstance(tgt, sympy.Symbol) and tgt in self.size_like:
  6140. self.size_like.add(a)
  6141. # Now, decide if we will do the substitution.
  6142. #
  6143. # - If the source has a non-trivial range, only substitute if
  6144. # we preserve this range. Note that we may have propagated
  6145. # the src_range to free variables in tgt when tgt is univariate
  6146. # and we could find an inverse, which helps us achieve this.
  6147. # This ensures we never "forget" about user defined ranges,
  6148. # even if they end up being defined on composite formulas
  6149. # like s0 + s1.
  6150. #
  6151. # - If the variable is unbacked, only substitute if the substitution
  6152. # would preserve the bounds also under size-like-ness conditions.
  6153. if not tgt_bound.issubset(src_bound):
  6154. self.log.debug(
  6155. "skipped set_replacement %s = %s (%s) [%s not subset of %s]",
  6156. a,
  6157. tgt,
  6158. msg,
  6159. tgt_bound,
  6160. src_bound,
  6161. )
  6162. return
  6163. elif a in self.size_like:
  6164. tgt_bound_so = self.bound_sympy(tgt, size_oblivious=True)
  6165. src_bound_so = self.bound_sympy(a, size_oblivious=True)
  6166. if not tgt_bound_so.issubset(src_bound_so):
  6167. self.log.debug(
  6168. "skipped set_replacement %s = %s (%s) "
  6169. "[%s not subset of %s (size-oblivious conditions)]",
  6170. a,
  6171. tgt,
  6172. msg,
  6173. tgt_bound_so,
  6174. src_bound_so,
  6175. )
  6176. return
  6177. if isinstance(tgt, (sympy.Integer, sympy.Float)):
  6178. # specializing to a constant, which is likely unexpected (unless
  6179. # you specified dynamic=True)
  6180. user_tb = TracingContext.extract_stack()
  6181. trace_structured(
  6182. "symbolic_shape_specialization",
  6183. metadata_fn=lambda: {
  6184. "symbol": repr(a),
  6185. "sources": [s.name for s in self.var_to_sources.get(a, [])],
  6186. "value": repr(tgt),
  6187. "reason": msg,
  6188. "stack": structured.from_traceback(
  6189. CapturedTraceback.extract(skip=1).summary()
  6190. ),
  6191. "user_stack": (
  6192. structured.from_traceback(user_tb) if user_tb else None
  6193. ),
  6194. },
  6195. )
  6196. for source in self.var_to_sources.get(a, []):
  6197. if user_tb:
  6198. self.specialization_stacks[source] = user_tb
  6199. if config.print_specializations:
  6200. self.log.warning(
  6201. "Specializing %s to %s", self.var_to_sources[a][0].name, tgt
  6202. )
  6203. self.log.debug("SPECIALIZATION", stack_info=True)
  6204. log.info("set_replacement %s = %s (%s) %s", a, tgt, msg, tgt_bound)
  6205. self.replacements[a] = tgt
  6206. # NB: the replacement may get refined, but the user will find the
  6207. # FIRST one most useful (TODO: Maybe we could consider tracking all of
  6208. # them)
  6209. if a not in self.replacements_slocs:
  6210. self.replacements_slocs[a] = self._get_sloc()
  6211. self._update_version_counter()
  6212. # When specializing 'a == tgt', the equality should be also conveyed to
  6213. # Z3, in case an expression uses 'a'.
  6214. self._add_target_expr(sympy.Eq(a, tgt, evaluate=False))
  6215. def _add_divisible(self, expr: sympy.Expr) -> None:
  6216. self.divisible.add(expr)
  6217. self._update_version_counter()
  6218. @_lru_cache
  6219. @record_shapeenv_event()
  6220. def _find(self, a: sympy.Symbol) -> sympy.Expr:
  6221. """
  6222. Implements a DSU-like algorithm to find the variable that represents a
  6223. Also handles transitive non-identity replacements.
  6224. a: b + c
  6225. c: d
  6226. """
  6227. if a not in self.replacements:
  6228. return a
  6229. res = self.replacements[a]
  6230. cur_replace = {s: self._find(s) for s in res.free_symbols}
  6231. replaced, changed = self.replacements[a]._xreplace(cur_replace)
  6232. if changed:
  6233. self._set_replacement(a, replaced, "find")
  6234. return self.replacements[a]
  6235. @lru_cache(256)
  6236. def _maybe_guard_rel(self, expr: sympy.Expr) -> None:
  6237. """
  6238. The relational guard is guarded to be true. Use this information to
  6239. simplify shapes (i.e. a == b or a % 5 == 0)
  6240. """
  6241. if isinstance(expr, sympy.And):
  6242. for arg in expr.args:
  6243. self._maybe_guard_rel(arg)
  6244. return
  6245. elif not isinstance(expr, sympy.Rel):
  6246. return
  6247. # A good example of what goes wrong if you don't do this is
  6248. # python test/functorch/test_aotdispatch.py -k
  6249. # test_aot_autograd_symbolic_module_exhaustive_nn_LazyConv3d_cpu_float32
  6250. if isinstance(expr, sympy.Ne):
  6251. return
  6252. free = list(expr.free_symbols)
  6253. if len(free) == 0:
  6254. raise AssertionError(
  6255. f"The expression should not be static by this point: {expr}"
  6256. )
  6257. # In case of really gnarly expression, we don't blow up
  6258. if len(free) > 5:
  6259. return
  6260. # Prioritize unbacked symints for solving by ordering them last.
  6261. # Prefer to simplify out lexicographically higher symbols (i.e. simplify out s4 over s3).
  6262. # (NB: this unfortunately isn't strictly equivalent to simplifying out newer symbols)
  6263. # Prefer to simplify out symbols with ephemeral sources.
  6264. def _smart_symbol_sort(x: sympy.Symbol) -> tuple[int, int, str]:
  6265. has_only_ephemeral_sources = x in self.var_to_sources and all(
  6266. s.is_ephemeral() for s in self.var_to_sources[x]
  6267. )
  6268. hint = self.backed_var_to_val.get(x)
  6269. if hint is None or isinstance(hint, SingletonInt):
  6270. # NB: size_hint is int, not sympy.Expr, do not use int_oo here.
  6271. # SingletonInt is used to represent jagged/nested tensor dimensions
  6272. # (e.g. the irregular ragged dimension). It cannot be converted to
  6273. # int, so we treat it the same as an unknown size. This matches the
  6274. # behavior of size_hint(), which returns None for SingletonInt.
  6275. size = sys.maxsize
  6276. elif symbol_is_type(x, SymT.SIZE):
  6277. size = int(hint)
  6278. else:
  6279. size = sys.maxsize
  6280. name = x.name
  6281. # 1 puts ephemeral sourced symbols first when sorting in reverse
  6282. return (1 if has_only_ephemeral_sources else 0, size, name)
  6283. free = sorted(free, key=_smart_symbol_sort, reverse=True) # type: ignore[attr-defined]
  6284. lhs = expr.lhs
  6285. rhs = expr.rhs
  6286. self._refine_ranges(expr)
  6287. # The rest of this stuff is for equality only
  6288. if not isinstance(expr, sympy.Eq):
  6289. return
  6290. if not expr.has(Mod):
  6291. try:
  6292. floor_div_atoms = lhs.atoms(FloorDiv).union(rhs.atoms(FloorDiv))
  6293. if len(floor_div_atoms) > 0 and any(
  6294. a.divisor != 1 for a in floor_div_atoms
  6295. ):
  6296. raise NotImplementedError
  6297. # Never replace unbacked symbols with other unbacked symbols that are
  6298. # not function arguments. (ex:mark_unbacked symbols are fine to replace
  6299. # other unbacked, but not those coming from .item() calls).
  6300. # This is error prone because you can cause references to
  6301. # unbacked symbols to time travel backwards. E.g.,
  6302. #
  6303. # u1 = x.item()
  6304. # ... use of u1 ...
  6305. # u2 = y.item()
  6306. # u3 = z.item()
  6307. # torch._check(u1 == u2 + u3)
  6308. #
  6309. # If you replace u1 with u2 + u3, then the use of u1 now
  6310. # references u2 and u3 prior to them actually being bound at
  6311. # runtime. It's pretty inconvenient to setup control
  6312. # dependencies for substitutions, so ban it entirely.
  6313. def trivial_solve(lhs: sympy.Expr, rhs: sympy.Expr) -> bool:
  6314. if isinstance(lhs, sympy.Symbol):
  6315. if free_unbacked_symbols(
  6316. lhs
  6317. ) and not _free_non_source_unbacked_symbols(
  6318. rhs, self.unbacked_inputs
  6319. ):
  6320. return True
  6321. if symbol_is_type(lhs, SymT.FLOAT):
  6322. return True
  6323. # TODO: Maybe trivial solutions for int should also be
  6324. # done?
  6325. return False
  6326. # short-circuit when no solving is needed
  6327. if trivial_solve(lhs, rhs):
  6328. self._set_replacement(lhs, self._find(rhs), "trivial_lhs")
  6329. elif trivial_solve(rhs, lhs):
  6330. self._set_replacement(rhs, self._find(lhs), "trivial_rhs")
  6331. else:
  6332. r = try_solve(expr, free[0], floordiv_inequality=False)
  6333. if r is not None and all(
  6334. t.is_integer for t in sympy.preorder_traversal(r[1])
  6335. ):
  6336. new_var = self._find(r[1])
  6337. ok = len(free_unbacked_symbols(new_var)) == 0
  6338. if ok:
  6339. self._set_replacement(free[0], new_var, "solve")
  6340. except NotImplementedError:
  6341. pass
  6342. else:
  6343. # expression has mod.
  6344. mod_expr = next(iter(expr.atoms(Mod)))
  6345. try:
  6346. r = try_solve(expr, mod_expr, floordiv_inequality=False)
  6347. if r is not None and r[1] == 0:
  6348. self._add_divisible(mod_expr)
  6349. except NotImplementedError:
  6350. pass
  6351. return
  6352. # See: Note - On 0/1 specialization
  6353. def _default_value_range(
  6354. self, do_not_specialize_zero_one: bool = False
  6355. ) -> ValueRanges:
  6356. lower = 0 if (do_not_specialize_zero_one or not self.specialize_zero_one) else 2
  6357. return ValueRanges(lower, int_oo)
  6358. def _default_unspecified_value_range(self) -> ValueRanges:
  6359. return ValueRanges.unknown_int()
  6360. @_lru_cache
  6361. def _simplify_floor_div(self, expr: sympy.Expr) -> sympy.Expr:
  6362. floor_divs = tuple(expr.atoms(FloorDiv))
  6363. # we expect floor_divs to be exact,
  6364. # and thus add the guards for the exact floordivs,
  6365. # even if tracing doesn't require them otherwise
  6366. for fd in reversed(floor_divs):
  6367. base, divisor = fd.args
  6368. mod_expr = Mod(base, divisor)
  6369. eq_expr = sympy.Eq(mod_expr, 0)
  6370. # add necessary mod guards
  6371. self.evaluate_expr(eq_expr)
  6372. return self.simplify(expr)
  6373. # We're about to add a guard/runtime assert, check if the ShapeEnv is frozen
  6374. # and if so issue a warning
  6375. def _check_frozen(self, expr: sympy.Basic, concrete_val: sympy.Basic) -> None:
  6376. if self.frozen:
  6377. self.counter["ignored_backward_guard"] += 1
  6378. signpost_event(
  6379. "dynamic",
  6380. "evaluate_expr_frozen",
  6381. {
  6382. **self.co_fields,
  6383. "ignored_guard": f"{expr} == {concrete_val}",
  6384. # no version = original state (this signpost is expected)
  6385. # version 2 = dynamic backwards is eagerly compiled
  6386. "version": 2,
  6387. },
  6388. )
  6389. log.info(
  6390. "Ignored guard %s == %s, this could result in accuracy problems",
  6391. expr,
  6392. concrete_val,
  6393. # only print stack trace when debug mode is on (e.g. TORCH_LOGS="dynamic")
  6394. stack_info=log.getEffectiveLevel() < logging.WARNING,
  6395. )
  6396. def _get_user_frame(self) -> Optional[types.FrameType]:
  6397. frame = inspect.currentframe()
  6398. while frame is not None:
  6399. if frame.f_code.co_filename not in uninteresting_files():
  6400. return frame
  6401. frame = frame.f_back
  6402. return frame
  6403. def _get_stack_summary(
  6404. self, is_debug: bool = False, framework_loc: Optional[str] = None
  6405. ) -> tuple[SLoc, str]:
  6406. floc: Optional[Union[str, traceback.FrameSummary]] = framework_loc
  6407. if floc is None:
  6408. frame = self._get_user_frame()
  6409. try:
  6410. if frame is not None:
  6411. floc = traceback.FrameSummary(
  6412. frame.f_code.co_filename,
  6413. frame.f_lineno,
  6414. frame.f_code.co_name,
  6415. )
  6416. finally:
  6417. del frame
  6418. # NB: this stack is truncated, but it's fine because the main
  6419. # stack_info will give you the rest of the info you need
  6420. maybe_user_loc = None
  6421. user_tb = TracingContext.extract_stack()
  6422. if user_tb:
  6423. idx = len(user_tb) - 1
  6424. while idx > 0 and user_tb[idx].filename in uninteresting_files():
  6425. idx -= 1
  6426. maybe_user_loc = format_frame(user_tb[idx], line=True)
  6427. maybe_extra_debug = ""
  6428. if is_debug and user_tb:
  6429. maybe_extra_debug = (
  6430. "\nUser Stack (most recent call last):\n"
  6431. + " (snipped, see stack below for prefix)\n"
  6432. + "".join(traceback.format_list(user_tb))
  6433. )
  6434. if is_debug and config.extended_debug_cpp:
  6435. cpp_stack = CapturedTraceback.extract(cpp=True)
  6436. maybe_extra_debug += "\nC++ stack trace:\n" + "".join(cpp_stack.format())
  6437. elif is_debug:
  6438. maybe_extra_debug += (
  6439. "\nFor C++ stack trace, run with TORCHDYNAMO_EXTENDED_DEBUG_CPP=1"
  6440. )
  6441. return SLoc(floc, maybe_user_loc), maybe_extra_debug
  6442. # Pass in framework_loc to override the framework location info
  6443. def _get_sloc(self, framework_loc: Optional[str] = None) -> SLoc:
  6444. sloc, _ = self._get_stack_summary(framework_loc=framework_loc)
  6445. return sloc
  6446. def _generate_unique_id(self, source_name: str) -> int:
  6447. attempt = int(hashlib.sha256(source_name.encode()).hexdigest(), 16) % 100
  6448. while attempt in self.unique_ids:
  6449. attempt += 1
  6450. self.unique_ids.add(attempt)
  6451. return attempt
  6452. def _find_frame_locals(self) -> _FrameLocalResult:
  6453. """
  6454. Given the current user code frame, finds the relevant lines of code,
  6455. values of symbolic locals, and free symbols involved.
  6456. """
  6457. frame_locals: dict[str, Any] = {}
  6458. frame_symbols: dict[str, str] = {}
  6459. if (
  6460. frame := _find_user_code_frame()
  6461. ) is None or frame.f_code.co_filename == "<string>":
  6462. return _FrameLocalResult()
  6463. # find bytecode instructions relevant to the frame
  6464. instructions = list(dis.Bytecode(frame.f_code))
  6465. co_lines, offset = inspect.getsourcelines(frame.f_code)
  6466. start, end, cur = None, None, None
  6467. # pyrefly: ignore [bad-assignment]
  6468. for i, instr in enumerate(instructions):
  6469. if instr.starts_line is not None:
  6470. cur = instr.starts_line
  6471. if cur != frame.f_lineno:
  6472. continue
  6473. if start is None:
  6474. start = end = i
  6475. else:
  6476. end = i
  6477. if start is None or end is None: # no instructions found
  6478. return _FrameLocalResult()
  6479. # track involved locals and free symbols
  6480. def go(x: Any) -> Optional[str]:
  6481. if isinstance(x, torch.Tensor):
  6482. for y in x.size():
  6483. go(y)
  6484. for y in x.stride():
  6485. go(y)
  6486. go(x.storage_offset())
  6487. return (
  6488. f"Tensor(shape: {x.size()}, "
  6489. f"stride: {x.stride()}, "
  6490. f"storage_offset: {x.storage_offset()})"
  6491. )
  6492. elif isinstance(x, (SymBool, SymInt, SymFloat)):
  6493. for s in x.node.expr.free_symbols:
  6494. if str(s) in frame_symbols: # type: ignore[operator]
  6495. continue
  6496. if s in self.var_to_sources:
  6497. frame_symbols[str(s)] = self.var_to_sources[s][0].name # type: ignore[assignment]
  6498. return str(x)
  6499. return None
  6500. # go through instructions, seeing linenos & involved locals
  6501. last_lineno = frame.f_lineno
  6502. for instr in instructions[start : end + 1]:
  6503. if (lineno := instr.starts_line) is not None:
  6504. last_lineno = max(last_lineno, lineno)
  6505. if isinstance(instr.argval, str) and instr.argval in frame.f_locals:
  6506. flat_locals = pytree.tree_flatten(frame.f_locals[instr.argval])[0]
  6507. frame_locals[instr.argval] = [
  6508. go(flat_local) for flat_local in flat_locals
  6509. ]
  6510. # store LOC
  6511. locs = co_lines[frame.f_lineno - offset : last_lineno + 1 - offset]
  6512. if not locs:
  6513. return _FrameLocalResult()
  6514. indent = len(locs[0]) - len(locs[0].lstrip())
  6515. frame_loc = "".join([loc[indent:] for loc in locs]).strip() # type: ignore[assignment]
  6516. return _FrameLocalResult(
  6517. loc=frame_loc, locals=frame_locals, symbols=frame_symbols
  6518. )
  6519. def _log_guard(self, prefix: str, g: SympyBoolean, forcing_spec: bool) -> None:
  6520. dtrace_structured(
  6521. "guard_added",
  6522. metadata_fn=lambda: {
  6523. "expr": str(g),
  6524. "prefix": prefix,
  6525. "expr_node_id": self._expr_sym_node_id,
  6526. "user_stack": structured.get_user_stack(3),
  6527. "stack": structured.get_framework_stack(3),
  6528. "symbol_to_sources": {
  6529. str(v): k
  6530. for k, v in self.source_to_var.items()
  6531. if v in g.free_symbols
  6532. },
  6533. "frame_locals": asdict(self._find_frame_locals()),
  6534. },
  6535. )
  6536. trace_structured(
  6537. "guard_added_fast",
  6538. metadata_fn=lambda: {
  6539. "expr": str(g),
  6540. "user_stack": structured.from_traceback(TracingContext.extract_stack()),
  6541. "stack": structured.from_traceback(
  6542. CapturedTraceback.extract(skip=1).summary()
  6543. ),
  6544. },
  6545. )
  6546. if self.log.isEnabledFor(logging.INFO):
  6547. str_g = str(g)
  6548. is_debug = (
  6549. config.extended_debug_guard_added is not None
  6550. and str_g == config.extended_debug_guard_added
  6551. )
  6552. sloc, maybe_extra_debug = self._get_stack_summary(is_debug)
  6553. maybe_more_info = ""
  6554. if not is_debug:
  6555. maybe_more_info = (
  6556. ", for more info run with "
  6557. f'TORCHDYNAMO_EXTENDED_DEBUG_GUARD_ADDED="{str_g}"'
  6558. )
  6559. self.log.info(
  6560. "%s %s [guard added] %s%s%s",
  6561. prefix if not forcing_spec else f"{prefix} (forcing_spec)",
  6562. str_g,
  6563. sloc,
  6564. maybe_more_info,
  6565. maybe_extra_debug,
  6566. stack_info=is_debug,
  6567. )
  6568. # A local variable to evaluate_expr stored in the class to avoid
  6569. # using it for the lru_cache that is on top of it since it does
  6570. # not effect the results. When needed its read directly.
  6571. _expr_sym_node_id: Optional[int] = None
  6572. def evaluate_sym_node(
  6573. self,
  6574. sym_node: SymNode,
  6575. size_oblivious: bool = False,
  6576. fallback_value: Optional[bool] = None,
  6577. ) -> sympy.Basic:
  6578. """
  6579. Given a a SymNode, evaluates sym_node.expr, adding guards if necessary.
  6580. """
  6581. self._expr_sym_node_id = id(sym_node)
  6582. return self.evaluate_expr(
  6583. sym_node.expr,
  6584. sym_node.hint,
  6585. sym_node.fx_node,
  6586. size_oblivious,
  6587. fallback_value=fallback_value,
  6588. )
  6589. def _is_python_assert(self) -> bool:
  6590. # Check if this boolean is used in an assertion, bytecode pattern for
  6591. # assertions is pretty stable for Python 3.7--3.13, ported with minimal
  6592. # changes from torch/fx/proxy.py
  6593. # Bytecode pattern for `assert` statements:
  6594. # TO_BOOL / COMPARE_OP # Only for Python >= 3.13
  6595. # POP_JUMP_IF_TRUE
  6596. # LOAD_ASSERTION_ERROR
  6597. # RAISE_VARARGS
  6598. frame = self._get_user_frame()
  6599. if frame is None:
  6600. raise AssertionError("frame must not be None")
  6601. insts = list(dis.get_instructions(frame.f_code))
  6602. if sys.version_info >= (3, 11):
  6603. # For Python >= 3.11, instructions can be 2-4 bytes long.
  6604. from bisect import bisect_left
  6605. cur = bisect_left(insts, frame.f_lasti, key=lambda x: x.offset)
  6606. else:
  6607. # For Python <= 3.10, instructions are always 2 bytes.
  6608. cur = frame.f_lasti // 2
  6609. if sys.version_info >= (3, 13):
  6610. if insts[cur].opname in ("TO_BOOL", "COMPARE_OP"):
  6611. # Peek 1 instruction further.
  6612. cur += 1
  6613. assert_insts = torch._dynamo.symbolic_convert.get_assert_bytecode_sequence(
  6614. False
  6615. )
  6616. cur_insts = insts[cur + 1 : cur + 1 + len(assert_insts)]
  6617. cur_insts = [inst.opname for inst in cur_insts]
  6618. return cur_insts == assert_insts
  6619. def _log_real_tensor_propagation(
  6620. self, orig_expr: sympy.Basic, unsound_result: sympy.Basic
  6621. ) -> None:
  6622. log.warning(
  6623. "propagate_real_tensors evaluate_expr(%s) -> %s",
  6624. orig_expr,
  6625. unsound_result,
  6626. )
  6627. trace_structured(
  6628. "propagate_real_tensors",
  6629. metadata_fn=lambda: {
  6630. "expr": repr(orig_expr),
  6631. "result": repr(unsound_result),
  6632. "stack": structured.from_traceback(
  6633. CapturedTraceback.extract(skip=1).summary()
  6634. ),
  6635. },
  6636. )
  6637. dtrace_structured(
  6638. "propagate_real_tensors_provenance",
  6639. metadata_fn=lambda: {
  6640. "expr": repr(orig_expr),
  6641. "result": repr(unsound_result),
  6642. "expr_node_id": self._expr_sym_node_id,
  6643. "user_stack": structured.get_user_stack(3),
  6644. "stack": structured.get_framework_stack(3),
  6645. "symbol_to_sources": {
  6646. str(v): k
  6647. for k, v in self.source_to_var.items()
  6648. if v in orig_expr.free_symbols
  6649. },
  6650. "frame_locals": asdict(self._find_frame_locals()),
  6651. },
  6652. )
  6653. def evaluate_expr(
  6654. self,
  6655. orig_expr: sympy.Basic,
  6656. hint: Optional[Union[int, bool, float]] = None,
  6657. fx_node: Optional[torch.fx.Node] = None,
  6658. size_oblivious: bool = False,
  6659. fallback_value: Optional[bool] = None,
  6660. *,
  6661. forcing_spec: bool = False,
  6662. ) -> sympy.Basic:
  6663. """
  6664. Given an expression, evaluates it, adding guards if necessary
  6665. When fallback_value is not None the function return fallback_value instead of failing with data dependent error.
  6666. """
  6667. # Add extra state that evaluate_expr() depends on.
  6668. suppress_guards_tls = ShapeEnv._suppress_guards_tls()
  6669. return self._inner_evaluate_expr(
  6670. orig_expr,
  6671. hint,
  6672. fx_node,
  6673. size_oblivious,
  6674. forcing_spec,
  6675. suppress_guards_tls,
  6676. fallback_value,
  6677. )
  6678. @lru_cache(256)
  6679. @record_shapeenv_event(save_tracked_fakes=True, name="evaluate_expr")
  6680. def _inner_evaluate_expr(
  6681. self,
  6682. orig_expr: sympy.Basic,
  6683. hint: Optional[Union[int, bool, float]],
  6684. fx_node: Optional[torch.fx.Node],
  6685. size_oblivious: bool,
  6686. forcing_spec: bool,
  6687. _suppress_guards_tls: bool,
  6688. fallback_value: Optional[bool] = None,
  6689. ) -> sympy.Basic:
  6690. try:
  6691. return self._evaluate_expr(
  6692. orig_expr,
  6693. hint,
  6694. fx_node,
  6695. size_oblivious,
  6696. fallback_value,
  6697. forcing_spec=forcing_spec,
  6698. )
  6699. except Exception as e:
  6700. if isinstance(e, GuardOnDataDependentSymNode):
  6701. pass
  6702. else:
  6703. self.log.warning(
  6704. "failed during evaluate_expr(%s, hint=%s, size_oblivious=%s, forcing_spec=%s",
  6705. orig_expr,
  6706. hint,
  6707. size_oblivious,
  6708. forcing_spec,
  6709. )
  6710. raise
  6711. def _log_suppressed_dde(self, a: SymBool, assumed_value: bool) -> None:
  6712. sloc, extra = self._get_stack_summary(True)
  6713. log.info(
  6714. "could not evaluate %s due to data dependency, it was assumed to be %s with no runtime assertions %s %s",
  6715. a,
  6716. assumed_value,
  6717. sloc,
  6718. extra,
  6719. )
  6720. def _evaluate_expr(
  6721. self,
  6722. orig_expr: sympy.Basic,
  6723. hint: Optional[Union[bool, int, float]] = None,
  6724. fx_node: Optional[torch.fx.Node] = None,
  6725. size_oblivious: bool = False,
  6726. fallback_value: Optional[bool] = None,
  6727. *,
  6728. forcing_spec: bool = False,
  6729. ) -> sympy.Basic:
  6730. # TODO: split conjunctions and evaluate them separately
  6731. if isinstance(
  6732. orig_expr,
  6733. (sympy.logic.boolalg.BooleanTrue, sympy.logic.boolalg.BooleanFalse),
  6734. ):
  6735. return orig_expr
  6736. # Don't track this one. (Because this cache is inside this function the
  6737. # cache only lasts for the invocation of this function call)
  6738. @functools.cache
  6739. def compute_concrete_val() -> sympy.Basic:
  6740. if hint is None:
  6741. # This is only ever called for expressions WITHOUT unbacked
  6742. # symbols
  6743. r = self.size_hint(orig_expr)
  6744. if r is None:
  6745. raise AssertionError("r must not be None")
  6746. return r
  6747. else:
  6748. return sympy.sympify(hint)
  6749. concrete_val: Optional[sympy.Basic]
  6750. # Check if:
  6751. # 1. 'translation_validation' is set
  6752. # 2. the corresponding 'fx_node' is not 'None'
  6753. # 3. the guard should not be suppressed
  6754. # 4. the guard doesn't contain backed symfloat symbols
  6755. # since z3 can't handle floats
  6756. # 5. fallback_value is none.
  6757. # If all of the above check, we create an FX node representing the
  6758. # actual expression to be guarded.
  6759. node = None
  6760. fresh = False
  6761. if (
  6762. self._translation_validation_enabled
  6763. and fx_node is not None
  6764. and not self._suppress_guards_tls()
  6765. and not size_oblivious
  6766. and not any(symbol_is_type(s, SymT.FLOAT) for s in orig_expr.free_symbols)
  6767. and fallback_value is None
  6768. ):
  6769. # TODO: does this even worked with unbacked :think:
  6770. concrete_val = compute_concrete_val()
  6771. if concrete_val is sympy.true:
  6772. node, fresh = self._create_fx_call_function(torch._assert, (fx_node,))
  6773. elif concrete_val is sympy.false:
  6774. neg, _ = self._create_fx_call_function(operator.not_, (fx_node,))
  6775. node, fresh = self._create_fx_call_function(torch._assert, (neg,))
  6776. else:
  6777. eql, _ = self._create_fx_call_function(
  6778. operator.eq, (fx_node, concrete_val)
  6779. )
  6780. node, fresh = self._create_fx_call_function(torch._assert, (eql,))
  6781. if node is None:
  6782. raise AssertionError("node must not be None")
  6783. # If this is a fresh node, we have to remember the event index that
  6784. # corresponds to this assertion node.
  6785. # Reason: so that, given an assertion node, we can replay the ShapeEnv
  6786. # events until the point where this assertion node was freshly created.
  6787. if fresh:
  6788. self._add_fx_node_metadata(node)
  6789. # After creating the FX node corresponding to orig_expr, we must make sure that
  6790. # no error will be raised until the end of this function.
  6791. #
  6792. # Reason: the translation validation may become invalid otherwise.
  6793. #
  6794. # If an error is raised before the end of this function, we remove the FX node
  6795. # inserted, and re-raise the error.
  6796. guard = None
  6797. try:
  6798. if orig_expr.is_number:
  6799. self.log.debug("eval %s [trivial]", orig_expr)
  6800. if hint is not None:
  6801. if isinstance(hint, bool):
  6802. if orig_expr != hint:
  6803. raise AssertionError(f"{orig_expr} != {hint}")
  6804. else:
  6805. if not sympy.Eq(orig_expr, hint):
  6806. raise AssertionError(f"{orig_expr} != {hint}")
  6807. return orig_expr
  6808. expr = orig_expr
  6809. # Try to quickly evaluate trivially true/false comparisons
  6810. # using var_to_range, before calling expensive _maybe_evaluate_static.
  6811. fast_result = self._maybe_fast_eval_comparison(expr)
  6812. if fast_result is not None:
  6813. return fast_result
  6814. static_expr = self._maybe_evaluate_static(
  6815. expr, size_oblivious=size_oblivious
  6816. )
  6817. if static_expr is not None:
  6818. self.log.debug(
  6819. "eval %s == %s [statically known]",
  6820. (
  6821. f"size_oblivious({orig_expr})"
  6822. if size_oblivious
  6823. else size_oblivious
  6824. ),
  6825. static_expr,
  6826. )
  6827. if (
  6828. not size_oblivious
  6829. and config.backed_size_oblivious
  6830. and hint is not None
  6831. ):
  6832. # TODO: maybe reconcile this with use of counterfactual hints
  6833. # in unbacked case
  6834. if static_expr != hint:
  6835. raise AssertionError(f"{static_expr} != {hint}")
  6836. return static_expr
  6837. transmute_into_runtime_assert = False
  6838. concrete_val = None
  6839. if not (expr.free_symbols <= self.backed_var_to_val.keys()):
  6840. # TODO: dedupe this with _maybe_evaluate_static
  6841. # Attempt to eliminate the unbacked SymInt
  6842. new_expr = self._maybe_evaluate_static(expr, unbacked_only=True)
  6843. if new_expr is None:
  6844. raise AssertionError("new_expr must not be None")
  6845. if not (new_expr.free_symbols <= self.backed_var_to_val.keys()):
  6846. ok = False
  6847. # fallback_value is set when guard_or_true or guard_or_false are used.
  6848. if not ok and fallback_value is not None:
  6849. self._log_suppressed_dde(orig_expr, fallback_value)
  6850. return fallback_value
  6851. # real_tensor_prop_unbacked_vals is not None iff propagate_real_tensors is on.
  6852. # if propagate_real_tensors is on, we check the example values to generate (unsound_result)
  6853. # and if they pass we add a runtime assertions and continue.
  6854. if (
  6855. not ok
  6856. and self.real_tensor_prop_unbacked_vals
  6857. and not (
  6858. unsound_result := orig_expr.xreplace(
  6859. self.real_tensor_prop_unbacked_vals
  6860. ).xreplace(self.backed_var_to_val)
  6861. ).free_symbols
  6862. ):
  6863. self._log_real_tensor_propagation(orig_expr, unsound_result)
  6864. transmute_into_runtime_assert = True
  6865. concrete_val = unsound_result
  6866. ok = True
  6867. # Check if this is coming from a python assert statement, if so, convert it to a runtime assertion
  6868. # instead of failing.
  6869. if not ok and self.trace_asserts and self._is_python_assert():
  6870. concrete_val = sympy.true
  6871. transmute_into_runtime_assert = True
  6872. ok = True
  6873. if not ok:
  6874. raise self._make_data_dependent_error(
  6875. expr.xreplace(self.backed_var_to_val),
  6876. expr,
  6877. expr_sym_node_id=self._expr_sym_node_id,
  6878. )
  6879. else:
  6880. expr = new_expr
  6881. if concrete_val is None:
  6882. concrete_val = compute_concrete_val()
  6883. self._check_frozen(expr, concrete_val)
  6884. if (
  6885. config.inject_EVALUATE_EXPR_flip_equality_TESTING_ONLY
  6886. and isinstance(hint, bool)
  6887. and isinstance(expr, (sympy.Eq, sympy.Ne))
  6888. ):
  6889. expr = sympy.Not(expr)
  6890. # Turn this into a boolean expression, no longer need to consult
  6891. # concrete_val
  6892. if concrete_val is sympy.true:
  6893. g = cast(SympyBoolean, expr)
  6894. elif concrete_val is sympy.false:
  6895. g = sympy.Not(expr)
  6896. else:
  6897. g = sympy.Eq(expr, concrete_val) # type: ignore[arg-type]
  6898. if transmute_into_runtime_assert:
  6899. self.guard_or_defer_runtime_assert(
  6900. g, f"propagate_real_tensors: {orig_expr} == {concrete_val}"
  6901. )
  6902. return concrete_val
  6903. if not self._suppress_guards_tls():
  6904. self._log_guard("eval", g, forcing_spec=forcing_spec)
  6905. # TODO: If we successfully eliminate a symbol via equality, it
  6906. # is not actually necessary to save a guard for the equality,
  6907. # as we will implicitly generate a guard when we match that
  6908. # input against the symbol. Probably the easiest way to
  6909. # implement this is to have maybe_guard_rel return a bool
  6910. # saying if it "subsumed" the guard (and therefore the guard
  6911. # is no longer necessary)
  6912. self._maybe_guard_rel(g)
  6913. if (
  6914. torch.compiler.is_exporting()
  6915. and self.prefer_deferred_runtime_asserts_over_guards
  6916. ):
  6917. # it's fine to defer simple guards here without checking,
  6918. # the _maybe_guard_rel() call above will set replacements if possible,
  6919. # and so the result here will be statically known
  6920. self.guard_or_defer_runtime_assert(g, f"evaluate_expr: {orig_expr}")
  6921. else:
  6922. # at this point, we've evaluated the concrete expr value, and have
  6923. # flipped/negated the guard if necessary. Now we know what to guard
  6924. # or defer to runtime assert on.
  6925. guard = ShapeGuard(
  6926. g, self._get_sloc(), size_oblivious=size_oblivious
  6927. )
  6928. self.guards.append(guard)
  6929. self.axioms.update(dict(self.get_implications(self.simplify(g))))
  6930. else:
  6931. self._log_guard("eval [guard suppressed]", g, forcing_spec=forcing_spec)
  6932. except Exception:
  6933. if fresh:
  6934. self._remove_fx_node(node)
  6935. raise
  6936. if not self._suppress_guards_tls():
  6937. if guard is not None: # we might have deferred this to runtime assert
  6938. for s in g.free_symbols:
  6939. self.symbol_guard_counter[s] += 1
  6940. # Forcing_spec to avoid infinite recursion
  6941. if (
  6942. not forcing_spec
  6943. and config.symbol_guard_limit_before_specialize is not None
  6944. and self.symbol_guard_counter[s]
  6945. > config.symbol_guard_limit_before_specialize
  6946. ):
  6947. # Force specialization
  6948. self.log.info(
  6949. "symbol_guard_limit_before_specialize=%s exceeded on %s",
  6950. config.symbol_guard_limit_before_specialize,
  6951. s,
  6952. )
  6953. self.evaluate_expr(s, forcing_spec=True)
  6954. return concrete_val
  6955. def cleanup(self) -> None:
  6956. """
  6957. Break reference cycles.
  6958. This destroys the stacks. If you really want to keep them, we
  6959. just need some way to break references on code objects.
  6960. """
  6961. for s in self.var_to_stack.values():
  6962. s.cleanup()
  6963. for ras in self.deferred_runtime_asserts.values():
  6964. for ra in ras:
  6965. ra.stack.cleanup()
  6966. def _should_skip_static_eval(self, expr: SympyBoolean) -> bool:
  6967. """Check if we should skip _maybe_evaluate_static for the given expression.
  6968. Skips static evaluation for single unbacked symbol >= 0 (or 0 <= symbol)
  6969. when the symbol has unknown range [-int_oo, int_oo].
  6970. This pattern is common during tracing and doesn't benefit from static evaluation
  6971. since the symbol has no constraints.
  6972. Note that the first time this is called value range will be updated and next time
  6973. it's called (if any) we would call _maybe_evaluate_static and it would return True.
  6974. """
  6975. unbacked_sym = None
  6976. if isinstance(expr, sympy.GreaterThan) and expr.rhs == 0:
  6977. unbacked_sym = expr.lhs
  6978. elif isinstance(expr, sympy.LessThan) and expr.lhs == 0:
  6979. unbacked_sym = expr.rhs
  6980. if isinstance(unbacked_sym, sympy.Symbol) and symbol_is_type(
  6981. unbacked_sym, SymT.UNBACKED_INT
  6982. ):
  6983. vr = self.var_to_range[unbacked_sym]
  6984. if vr.lower == -int_oo and vr.upper == int_oo:
  6985. return True
  6986. return False
  6987. @lru_cache(256)
  6988. @record_shapeenv_event(save_tracked_fakes=True)
  6989. def guard_or_defer_runtime_assert(
  6990. self, orig_expr: SympyBoolean, msg: str, fx_node: Optional[torch.fx.Node] = None
  6991. ) -> bool:
  6992. """
  6993. Adds a guard that orig_expr is True if we can or fall back to adding an assert
  6994. that is checked at runtime.
  6995. Args:
  6996. orig_expr (sympy.Expr): Boolean expression to assert is true
  6997. msg (str): Message to display on assertion failure
  6998. fx_node (Optional, torch.fx.Node): node in ``self.graph`` corresponding
  6999. to the expression, if applicable
  7000. """
  7001. expr = orig_expr
  7002. # TODO: split conjunctions and evaluate them separately
  7003. # Try to quickly evaluate trivially true/false comparisons
  7004. # using var_to_range, before calling expensive _maybe_evaluate_static.
  7005. fast_result = self._maybe_fast_eval_comparison(expr)
  7006. if fast_result is not None:
  7007. return bool(fast_result)
  7008. if self._should_skip_static_eval(expr):
  7009. new_expr = expr
  7010. else:
  7011. static_expr = self._maybe_evaluate_static(expr)
  7012. if static_expr is not None:
  7013. self.log.debug(
  7014. "runtime_assert %s == %s [statically known]", orig_expr, static_expr
  7015. )
  7016. # TODO: assert bool(static_expr)
  7017. return bool(static_expr)
  7018. # Attempt to eliminate the unbacked SymInt
  7019. new_expr = self._maybe_evaluate_static(expr, unbacked_only=True)
  7020. if new_expr is None:
  7021. raise AssertionError("new_expr must not be None")
  7022. if (
  7023. not self.prefer_deferred_runtime_asserts_over_guards
  7024. and new_expr.free_symbols <= self.backed_var_to_val.keys()
  7025. ):
  7026. # Do a normal guard
  7027. return self.evaluate_expr(new_expr, fx_node=fx_node)
  7028. # NB: Don't use new_expr as expr; it could contain gunk like shape0
  7029. # which we don't want to guard on
  7030. if (
  7031. self._translation_validation_enabled
  7032. and fx_node is not None
  7033. and not self._suppress_guards_tls()
  7034. ):
  7035. node, fresh = self._create_fx_call_function(torch._assert, (fx_node,))
  7036. if node is None:
  7037. raise AssertionError("node must not be None")
  7038. if fresh:
  7039. self._add_fx_node_metadata(node)
  7040. if not self._suppress_guards_tls():
  7041. self._log_guard("runtime_assert", orig_expr, forcing_spec=False)
  7042. # If you're here because of this assert, read Note [Backwards runtime asserts]
  7043. # in torch/_inductor/graph.py
  7044. if self.runtime_asserts_frozen:
  7045. log.debug("runtime_asserts_frozen but then got %s", expr)
  7046. self._check_frozen(expr, sympy.true)
  7047. # eliminate symbols on equality tests / refine ranges
  7048. self._maybe_guard_rel(expr)
  7049. # canonicalise to remove equations that are trivially equal
  7050. orig_expr = expr
  7051. expr = canonicalize_bool_expr(expr)
  7052. stack = CapturedTraceback.extract(skip=1)
  7053. ra = RuntimeAssert(expr, msg, stack)
  7054. # TODO: Do this in a way that is less janky than int(s.name[1:])
  7055. cands = sorted(
  7056. (s for s in expr.free_symbols if symbol_is_type(s, SymT.UNBACKED_INT)),
  7057. key=lambda s: int(s.name[1:]),
  7058. )
  7059. # Is None when prefer_deferred_runtime_asserts_over_guards=True
  7060. # and the guard in question has no unbacked SymInts in front
  7061. ix = cands[-1] if cands else None
  7062. self.deferred_runtime_asserts.setdefault(ix, []).append(ra)
  7063. self.axioms.update(dict(self.get_implications(self.simplify(expr))))
  7064. self.num_deferred_runtime_asserts += 1
  7065. self._update_version_counter()
  7066. else:
  7067. self._log_guard(
  7068. "runtime_assert [guard suppressed]", orig_expr, forcing_spec=False
  7069. )
  7070. return True
  7071. # Refines the ranges of the variables present in 'guard'.
  7072. #
  7073. # This function tries to refine the range of the variables inside
  7074. # 'guard' by reasoning about it. Specifically, when 'guard' is a
  7075. # 'sympy.Relational' operation.
  7076. #
  7077. # It does mainly 3 things:
  7078. # 1. Tries to isolate a variable in the left-hand side
  7079. # 2. Compute the value range of the right-hand side
  7080. # 3. Update the value range of the variable, if better
  7081. def _refine_ranges(self, expr: SympyBoolean) -> None:
  7082. expr = self.simplify(expr)
  7083. for symbol in expr.free_symbols:
  7084. if not isinstance(symbol, sympy.Symbol):
  7085. raise AssertionError(f"Expected sympy.Symbol, got {type(symbol)}")
  7086. if isinstance(self.backed_var_to_val.get(symbol, None), SingletonInt):
  7087. # Skip var_to_range logic for SingletonInt which is only used
  7088. # for jagged layout NestedTensors today
  7089. continue
  7090. r = try_solve(expr, symbol)
  7091. if r is None or not (symbol.is_integer and r[1].is_integer):
  7092. # Range refinement only supports integer symbols for now.
  7093. # There are lots of SymPy bugs when it comes to comparing
  7094. # reals and integers, so we skip that for now.
  7095. continue
  7096. r_expr, rhs = r
  7097. vr = self.var_to_range[symbol]
  7098. lower, upper = vr.lower, vr.upper
  7099. rhs_vr = bound_sympy(rhs, self.var_to_range)
  7100. # Let's suppose that we have a preexisting range for x [0, 100].
  7101. # Now, we issue a guard x > y, where the range for y is [50, 150].
  7102. # Then, lower = 0, rhs_vr.lower = 50 and therefore refinement can happen,
  7103. # refining x to [51, 100], since x must be greater than y, but the lowest
  7104. # y could be is 50.
  7105. #
  7106. # sympy.Eq may update both lower and upper bounds.
  7107. # sympy.G{t,e} may update the lower bound, only.
  7108. # sympy.L{t,e} may update the upper bound, only.
  7109. if lower <= rhs_vr.lower and isinstance(
  7110. r_expr, (sympy.Eq, sympy.Ge, sympy.Gt)
  7111. ):
  7112. # Strictly greater relations allow us to refine a bit more, since
  7113. # x < y implies that the lower bound for x is: y + 1.
  7114. lower = rhs_vr.lower + int(isinstance(r_expr, sympy.Gt))
  7115. if upper >= rhs_vr.upper and isinstance(
  7116. r_expr, (sympy.Eq, sympy.Le, sympy.Lt)
  7117. ):
  7118. upper = rhs_vr.upper - int(isinstance(r_expr, sympy.Lt))
  7119. # Do nothing if the new value range is no better than what we already have.
  7120. if vr == ValueRanges(lower, upper):
  7121. continue
  7122. # Updates the range and the guards corresponding to each bound of the symbol.
  7123. self._update_var_to_range(symbol, ValueRanges(lower, upper))
  7124. # If the range is refined to singleton, set replacement
  7125. if self.var_to_range[symbol].is_singleton():
  7126. self._set_replacement(
  7127. symbol,
  7128. self.var_to_range[symbol].lower,
  7129. "range_refined_to_singleton",
  7130. )
  7131. # Clears the cache, since this update can change the result.
  7132. self._maybe_evaluate_static.cache_clear()
  7133. @lru_cache(maxsize=None)
  7134. @record_shapeenv_event()
  7135. def constrain_symbol_range(
  7136. self, s: sympy.Symbol, compiler_min: int, compiler_max: int
  7137. ) -> None:
  7138. upd_vr = ValueRanges(compiler_min, compiler_max)
  7139. old_vr = self.var_to_range.get(s, ValueRanges.unknown())
  7140. self._update_var_to_range(s, upd_vr)
  7141. if (new_vr := self.var_to_range[s]) != old_vr:
  7142. log.info(
  7143. "constrain_symbol_range %s [%s, %s]", s, new_vr.lower, new_vr.upper
  7144. )
  7145. def _is_int(expr: object) -> TypeGuard[SymInt]:
  7146. return isinstance(expr, SymInt) and expr.node.expr.is_number
  7147. # WARNING: This is legacy, DO NOT USE
  7148. def _is_dim_dynamic(t: torch.Tensor, d: int) -> bool:
  7149. return hasattr(t, "_dynamo_dynamic_indices") and d in t._dynamo_dynamic_indices
  7150. class PropagateUnbackedSymInts(torch.fx.Interpreter):
  7151. def run_node(self, n: torch.fx.Node) -> Result:
  7152. """
  7153. Run an FX node, propagating unbacked Symbol bindings to the new fake tensor
  7154. """
  7155. from torch._guards import detect_fake_mode
  7156. result = super().run_node(n)
  7157. fake_mode = detect_fake_mode()
  7158. if fake_mode is None:
  7159. raise AssertionError("fake_mode must not be None")
  7160. rebind_unbacked(fake_mode.shape_env, n, result)
  7161. return result
  7162. def _find_user_code_frame() -> Optional[types.FrameType]:
  7163. frame = inspect.currentframe()
  7164. while frame is not None:
  7165. if not frame.f_code.co_filename.startswith(
  7166. os.path.dirname(inspect.getfile(torch)) + os.path.sep
  7167. ):
  7168. break
  7169. frame = frame.f_back
  7170. return frame
  7171. def _blame_user_code(e: Exception, frame: types.FrameType) -> None:
  7172. frame_summary = traceback.FrameSummary(
  7173. frame.f_code.co_filename,
  7174. frame.f_lineno,
  7175. frame.f_code.co_name,
  7176. )
  7177. msg = e.args[0]
  7178. msg += "\n\nThe following call raised this error:\n" + "".join(
  7179. traceback.StackSummary.from_list([frame_summary]).format()
  7180. )
  7181. e.args = (msg,)
  7182. class _PythonMsgPrinter(PythonPrinter):
  7183. """
  7184. Util printer that replaces sympy symbols with their source-level names
  7185. and renders sympy relational operators (e.g., Eq, Ne, Ge, Le) inline
  7186. (i.e., as ==, !=, >, <).
  7187. """
  7188. def __init__(self, src_map: dict[str, list[str]]) -> None:
  7189. super().__init__()
  7190. self.src_map = src_map
  7191. def _print_Symbol(self, sym: sympy.Symbol) -> str:
  7192. return self.src_map[sym.name][0]
  7193. def _suggest_torch_checks(
  7194. e: GuardOnDataDependentSymNode, src_map: defaultdict[str, list[str]]
  7195. ) -> None:
  7196. """
  7197. Enhances a GuardOnDataDependentSymNode error with suggested fixes using torch._check.
  7198. This function analyzes the condition that caused the data-dependent error and generates
  7199. user-friendly suggestions for fixing it by adding appropriate torch._check calls.
  7200. It handles special cases like non-negative checks with specific recommendations.
  7201. Args:
  7202. e: The GuardOnDataDependentSymNode error to enhance with suggestions
  7203. src_map: A mapping from symbol names to their corresponding source-level variable names
  7204. Returns:
  7205. None. Modifies the error message in-place by updating e.args[0].
  7206. """
  7207. # extract the unresolved condition on unbacked symints in the error
  7208. cond = e.cond
  7209. diff = ", ".join(s.name for s in cond.free_symbols if s.name not in src_map)
  7210. if diff:
  7211. log.warning("Unable to find user code corresponding to {%s}", diff)
  7212. return
  7213. printer = _PythonMsgPrinter(src_map)
  7214. msg = e.args[0]
  7215. msg += "\nTo fix the error, insert one of the following checks before this call:"
  7216. not_cond_str = printer.doprint(sympy.Not(cond))
  7217. # suggested fixes to resolve `cond` are to tell the compiler to assume
  7218. # either `cond` or its negation (the user will need to select which)
  7219. suggested_fixes = [
  7220. f"torch._check({printer.doprint(cond)})",
  7221. f"torch._check({not_cond_str})",
  7222. ]
  7223. for i, fix in enumerate(suggested_fixes):
  7224. msg += f"\n {i + 1}. {fix}"
  7225. src_mapped = ", ".join(
  7226. f"`{s}` with {' or '.join(src_map[s])}"
  7227. for s in sorted(s.name for s in cond.free_symbols)
  7228. )
  7229. msg += f"\n\n(These suggested fixes were derived by replacing {src_mapped} in {cond} and its negation.)"
  7230. e.args = (msg,)
  7231. def _suggest_fixes_for_data_dependent_error_non_strict(
  7232. e: GuardOnDataDependentSymNode,
  7233. ) -> None:
  7234. """
  7235. Given a raised data-dependent error, add the following to the error message:
  7236. 1. the closest user code location that raised the error;
  7237. 2. suggested fixes for the error in terms of live variables at that location.
  7238. """
  7239. # walk the stack up from the data-dependent error until a non-torch frame is found
  7240. frame = _find_user_code_frame()
  7241. if frame is not None:
  7242. # add frame info to error message
  7243. _blame_user_code(e, frame)
  7244. # map symbol names reachable via frame locals to their source-level names
  7245. src_map = defaultdict(list)
  7246. for var, val in frame.f_locals.items():
  7247. try:
  7248. tree_leaves_with_path = pytree.tree_leaves_with_path(val)
  7249. except ValueError:
  7250. log.warning(
  7251. "pytree.tree_leaves_with_path failed for value of type {%s} in local variable {%s}",
  7252. type(val),
  7253. var,
  7254. )
  7255. continue
  7256. # figure out how to access any symbol inside `val` through `var`
  7257. for path, leaf in tree_leaves_with_path:
  7258. name = var + pytree.keystr(path)
  7259. if isinstance(leaf, torch.SymInt):
  7260. src_map[str(leaf.node.expr)].append(name)
  7261. elif isinstance(leaf, torch.Tensor):
  7262. for i, dim in enumerate(leaf.shape):
  7263. if isinstance(dim, torch.SymInt):
  7264. src_map[str(dim.node.expr)].append(f"{name}.shape[{i}]")
  7265. # add suggested torch.check()s based on `src_map` to the error message
  7266. # replacing unbacked symints in the unresolved condition in the error
  7267. if isinstance(e.cond, sympy.logic.boolalg.Boolean):
  7268. _suggest_torch_checks(e, src_map)
  7269. @contextmanager
  7270. def _remove_effect_token_unbacked_bindings(
  7271. node: torch.fx.Node,
  7272. ) -> Generator[None, None, None]:
  7273. """
  7274. Temporarily modifies unbacked_bindings in a node's metadata by removing the first element
  7275. of each path, which corresponds to an effect token.
  7276. This is used when processing nodes that have effect tokens as the first element in their
  7277. unbacked_bindings paths. The context manager ensures that the original bindings are
  7278. restored after the operation is complete.
  7279. Args:
  7280. node: The FX node whose unbacked_bindings will be temporarily modified
  7281. Yields:
  7282. None
  7283. """
  7284. old_bindings = node.meta.get("unbacked_bindings", {})
  7285. # Remove the extra layer for effect token
  7286. new_bindings = {k: path[1:] if path else path for k, path in old_bindings.items()}
  7287. node.meta["unbacked_bindings"] = new_bindings
  7288. try:
  7289. yield
  7290. finally:
  7291. node.meta["unbacked_bindings"] = old_bindings
  7292. # This helper function is used in passes that insert runtime assertions in the graph.
  7293. # When accessing expressions representing input placeholders, we do not apply replacements
  7294. # since those inputs should be seen by assertions that use them to be inserted. The only replacement
  7295. # that we apply is unbacked renaming.
  7296. def _get_placeholder_expr(sym_node: SymNode) -> sympy.Expr:
  7297. shape_env = sym_node.shape_env
  7298. result = sym_node._expr
  7299. if result in shape_env.unbacked_renamings:
  7300. return shape_env.unbacked_renamings[result]
  7301. return result